@gethmy/agent 1.18.1 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1455 -1449
- package/dist/index.js +1455 -1449
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -730,1457 +730,1505 @@ var init_config_validation = __esm(() => {
|
|
|
730
730
|
}
|
|
731
731
|
};
|
|
732
732
|
});
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
var
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
upsertReviewedSha: () => upsertReviewedSha,
|
|
739
|
-
updateExistingPr: () => updateExistingPr,
|
|
740
|
-
resolvePrUrl: () => resolvePrUrl,
|
|
741
|
-
renameRemoteBranch: () => renameRemoteBranch,
|
|
742
|
-
remoteBranchExists: () => remoteBranchExists,
|
|
743
|
-
pushBranch: () => pushBranch,
|
|
744
|
-
mergePullRequest: () => mergePullRequest,
|
|
745
|
-
getPrStatus: () => getPrStatus,
|
|
746
|
-
getHeadSha: () => getHeadSha,
|
|
747
|
-
getBranchWebUrl: () => getBranchWebUrl,
|
|
748
|
-
findExistingPr: () => findExistingPr,
|
|
749
|
-
extractReviewedSha: () => extractReviewedSha,
|
|
750
|
-
extractPrUrl: () => extractPrUrl,
|
|
751
|
-
detectGitProvider: () => detectGitProvider,
|
|
752
|
-
deriveCiStatus: () => deriveCiStatus,
|
|
753
|
-
createPullRequest: () => createPullRequest,
|
|
754
|
-
checkPrMergeStatus: () => checkPrMergeStatus,
|
|
755
|
-
buildPrBody: () => buildPrBody
|
|
733
|
+
// ../harmony-shared/dist/branchRef.js
|
|
734
|
+
var BRANCH_REF_PATTERN, SAFE_GIT_REF_PATTERN;
|
|
735
|
+
var init_branchRef = __esm(() => {
|
|
736
|
+
BRANCH_REF_PATTERN = /Branch:\s*`([^`]+)`/;
|
|
737
|
+
SAFE_GIT_REF_PATTERN = /^[a-zA-Z0-9/_.+-]+$/;
|
|
756
738
|
});
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
}).trim();
|
|
765
|
-
if (url.includes("github.com"))
|
|
766
|
-
return "github";
|
|
767
|
-
if (url.includes("dev.azure.com") || url.includes("visualstudio.com"))
|
|
768
|
-
return "azure";
|
|
769
|
-
if (url.includes("gitlab.com") || /\bgitlab\b/.test(url))
|
|
770
|
-
return "gitlab";
|
|
771
|
-
if (url.includes("bitbucket.org"))
|
|
772
|
-
return "bitbucket";
|
|
773
|
-
return "unknown";
|
|
774
|
-
} catch {
|
|
775
|
-
return "unknown";
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
function validateGitProviderCli(provider, cwd) {
|
|
779
|
-
switch (provider) {
|
|
780
|
-
case "github": {
|
|
781
|
-
try {
|
|
782
|
-
execFileSync("gh", ["auth", "status"], { cwd, stdio: "pipe" });
|
|
783
|
-
} catch {
|
|
784
|
-
throw new Error("GitHub CLI (gh) is not authenticated. Run: gh auth login");
|
|
785
|
-
}
|
|
786
|
-
break;
|
|
787
|
-
}
|
|
788
|
-
case "azure": {
|
|
789
|
-
try {
|
|
790
|
-
execFileSync("az", ["--version"], { cwd, stdio: "pipe" });
|
|
791
|
-
} catch {
|
|
792
|
-
throw new Error("Azure CLI (az) not found. Install it: https://learn.microsoft.com/en-us/cli/azure/install-azure-cli");
|
|
793
|
-
}
|
|
794
|
-
try {
|
|
795
|
-
execFileSync("az", ["account", "show"], { cwd, stdio: "pipe" });
|
|
796
|
-
} catch {
|
|
797
|
-
throw new Error("Azure CLI is not authenticated. Run: az login");
|
|
798
|
-
}
|
|
799
|
-
break;
|
|
800
|
-
}
|
|
801
|
-
case "gitlab": {
|
|
802
|
-
try {
|
|
803
|
-
execFileSync("glab", ["auth", "status"], { cwd, stdio: "pipe" });
|
|
804
|
-
} catch {
|
|
805
|
-
throw new Error("GitLab CLI (glab) is not installed or not authenticated. Install: https://gitlab.com/gitlab-org/cli — then run: glab auth login");
|
|
806
|
-
}
|
|
807
|
-
break;
|
|
808
|
-
}
|
|
809
|
-
case "bitbucket":
|
|
810
|
-
case "unknown":
|
|
811
|
-
log.warn(TAG2, `Git provider "${provider}" — PR creation will be skipped (no CLI support)`);
|
|
812
|
-
break;
|
|
813
|
-
}
|
|
739
|
+
|
|
740
|
+
// ../harmony-shared/dist/cardLinks.js
|
|
741
|
+
var init_cardLinks = () => {};
|
|
742
|
+
// ../harmony-shared/dist/classification.js
|
|
743
|
+
function escalateTier(tier) {
|
|
744
|
+
const i = MODEL_TIERS.indexOf(tier);
|
|
745
|
+
return MODEL_TIERS[Math.min(i + 1, MODEL_TIERS.length - 1)];
|
|
814
746
|
}
|
|
815
|
-
function
|
|
816
|
-
return
|
|
747
|
+
function isModelTier(v) {
|
|
748
|
+
return typeof v === "string" && MODEL_TIERS.includes(v);
|
|
817
749
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
750
|
+
var MODEL_TIERS;
|
|
751
|
+
var init_classification = __esm(() => {
|
|
752
|
+
MODEL_TIERS = ["simple", "advanced", "research"];
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// ../harmony-shared/dist/commentSerializer.js
|
|
756
|
+
function sanitizeHeaderField(value) {
|
|
757
|
+
return value.replace(/[\]\r\n|<>]/g, " ").trim() || "—";
|
|
823
758
|
}
|
|
824
|
-
function
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
const sep = description ? `
|
|
830
|
-
` : "";
|
|
831
|
-
return `${description}${sep}${line}`;
|
|
759
|
+
function authorLabel(c) {
|
|
760
|
+
if (c.author_type === "agent")
|
|
761
|
+
return "AI agent";
|
|
762
|
+
const raw = c.author?.full_name || "teammate";
|
|
763
|
+
return sanitizeHeaderField(raw);
|
|
832
764
|
}
|
|
833
|
-
function
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
if (
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
if (typeof c.status === "string") {
|
|
842
|
-
if (c.status.toUpperCase() !== "COMPLETED") {
|
|
843
|
-
anyPending = true;
|
|
844
|
-
continue;
|
|
845
|
-
}
|
|
846
|
-
const conclusion = typeof c.conclusion === "string" ? c.conclusion.toUpperCase() : "";
|
|
847
|
-
if (["SUCCESS", "NEUTRAL", "SKIPPED"].includes(conclusion))
|
|
848
|
-
continue;
|
|
849
|
-
return "failure";
|
|
765
|
+
function criticalIds(comments) {
|
|
766
|
+
const keep = new Set;
|
|
767
|
+
for (const c of comments) {
|
|
768
|
+
if (c.comment_type === "decision")
|
|
769
|
+
keep.add(c.id);
|
|
770
|
+
if (c.supersedes_id) {
|
|
771
|
+
keep.add(c.id);
|
|
772
|
+
keep.add(c.supersedes_id);
|
|
850
773
|
}
|
|
851
|
-
if (
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
continue;
|
|
855
|
-
if (state === "PENDING") {
|
|
856
|
-
anyPending = true;
|
|
857
|
-
continue;
|
|
858
|
-
}
|
|
859
|
-
return "failure";
|
|
774
|
+
if (c.confirms_id) {
|
|
775
|
+
keep.add(c.id);
|
|
776
|
+
keep.add(c.confirms_id);
|
|
860
777
|
}
|
|
861
778
|
}
|
|
862
|
-
return
|
|
779
|
+
return keep;
|
|
863
780
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
781
|
+
function serializeCommentThread(comments, options = {}) {
|
|
782
|
+
const { heading = "Conversation", includeInstructions = true, activity = [], maxComments } = options;
|
|
783
|
+
const visible = comments.filter((c) => !c.deleted_at).slice().sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
784
|
+
if (visible.length === 0)
|
|
785
|
+
return "";
|
|
786
|
+
const indexById = new Map;
|
|
787
|
+
visible.forEach((c, i) => {
|
|
788
|
+
indexById.set(c.id, i + 1);
|
|
789
|
+
});
|
|
790
|
+
let rendered = visible;
|
|
791
|
+
let elidedCount = 0;
|
|
792
|
+
if (maxComments && visible.length > maxComments) {
|
|
793
|
+
const keep = criticalIds(visible);
|
|
794
|
+
const recentThreshold = visible.length - maxComments;
|
|
795
|
+
rendered = visible.filter((c, i) => i >= recentThreshold || keep.has(c.id));
|
|
796
|
+
elidedCount = visible.length - rendered.length;
|
|
867
797
|
}
|
|
868
|
-
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
798
|
+
const ref = (id) => {
|
|
799
|
+
const n = indexById.get(id);
|
|
800
|
+
return n ? `#${n}` : `#${id.slice(0, 8)}`;
|
|
801
|
+
};
|
|
802
|
+
const lines = [];
|
|
803
|
+
if (elidedCount > 0) {
|
|
804
|
+
lines.push({
|
|
805
|
+
at: visible[0]?.created_at ?? "",
|
|
806
|
+
text: `(${elidedCount} earlier comment(s) omitted for brevity)`
|
|
807
|
+
});
|
|
875
808
|
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
809
|
+
for (const c of rendered) {
|
|
810
|
+
const tags = [];
|
|
811
|
+
if (c.edited_at)
|
|
812
|
+
tags.push("edited");
|
|
813
|
+
if (c.reply_to_id)
|
|
814
|
+
tags.push(`reply to ${ref(c.reply_to_id)}`);
|
|
815
|
+
if (c.supersedes_id)
|
|
816
|
+
tags.push(`supersedes ${ref(c.supersedes_id)}`);
|
|
817
|
+
if (c.confirms_id)
|
|
818
|
+
tags.push(`confirms ${ref(c.confirms_id)}`);
|
|
819
|
+
if (c.resolved_at)
|
|
820
|
+
tags.push("resolved");
|
|
821
|
+
const tagStr = tags.length ? ` | ${tags.join(" | ")}` : "";
|
|
822
|
+
const header = `[${sanitizeHeaderField(ref(c.id))} | ${sanitizeHeaderField(c.author_type)} | ${authorLabel(c)} | ${sanitizeHeaderField(c.comment_type)} | ${sanitizeHeaderField(c.created_at)}${tagStr}]`;
|
|
823
|
+
const fencedBody = c.body.trim().replaceAll("<", "<").replaceAll(">", ">");
|
|
824
|
+
lines.push({
|
|
825
|
+
at: c.created_at,
|
|
826
|
+
text: `${header}
|
|
827
|
+
<comment-body>
|
|
828
|
+
${fencedBody}
|
|
829
|
+
</comment-body>`
|
|
830
|
+
});
|
|
880
831
|
}
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
await execFileAsync("gh", args, { cwd, encoding: "utf-8", timeout: 30000 });
|
|
885
|
-
}
|
|
886
|
-
function getHeadSha(cwd) {
|
|
887
|
-
try {
|
|
888
|
-
return execFileSync("git", ["rev-parse", "HEAD"], {
|
|
889
|
-
cwd,
|
|
890
|
-
encoding: "utf-8"
|
|
891
|
-
}).trim();
|
|
892
|
-
} catch {
|
|
893
|
-
return null;
|
|
832
|
+
for (const a of activity) {
|
|
833
|
+
const actor = a.actor ? `${a.actor} ` : "";
|
|
834
|
+
lines.push({ at: a.at, text: `· (system) ${a.at} — ${actor}${a.text}` });
|
|
894
835
|
}
|
|
836
|
+
lines.sort((a, b) => a.at.localeCompare(b.at));
|
|
837
|
+
const body = lines.map((l) => l.text).join(`
|
|
838
|
+
|
|
839
|
+
`);
|
|
840
|
+
const instruction = includeInstructions ? `
|
|
841
|
+
|
|
842
|
+
${CONFLICT_INSTRUCTION}` : "";
|
|
843
|
+
return `## ${heading} (oldest → newest)
|
|
844
|
+
|
|
845
|
+
${body}${instruction}`;
|
|
895
846
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
847
|
+
var CONFLICT_INSTRUCTION;
|
|
848
|
+
var init_commentSerializer = __esm(() => {
|
|
849
|
+
CONFLICT_INSTRUCTION = "When two comments conflict, prefer the latest created_at, UNLESS a later " + "comment explicitly confirms or restates the earlier finding. Evaluate " + "substance, not just recency. Cite the comment id(s) you relied on.";
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// ../harmony-shared/dist/constants.js
|
|
853
|
+
var TIMINGS;
|
|
854
|
+
var init_constants = __esm(() => {
|
|
855
|
+
TIMINGS = {
|
|
856
|
+
SEARCH_DEBOUNCE: 300,
|
|
857
|
+
AUTOSAVE_DEBOUNCE: 1000,
|
|
858
|
+
TOAST_DURATION: 3000,
|
|
859
|
+
QUERY_STALE_TIME: 1000 * 60 * 5,
|
|
860
|
+
QUERY_GC_TIME: 1000 * 60 * 60 * 24
|
|
861
|
+
};
|
|
862
|
+
});
|
|
863
|
+
// ../harmony-shared/dist/gateEvaluate.js
|
|
864
|
+
function isGateKind(value) {
|
|
865
|
+
return typeof value === "string" && GATE_KINDS.includes(value);
|
|
866
|
+
}
|
|
867
|
+
function isGateOperator(value) {
|
|
868
|
+
return typeof value === "string" && GATE_OPERATORS.includes(value);
|
|
869
|
+
}
|
|
870
|
+
function gateEvaluate(gateSpec, evidence) {
|
|
871
|
+
const structured = isPlainObject(evidence) ? evidence.structured ?? {} : {};
|
|
872
|
+
const safeStructured = isPlainObject(structured) ? structured : {};
|
|
873
|
+
if (!isPlainObject(gateSpec)) {
|
|
874
|
+
return {
|
|
875
|
+
passed: false,
|
|
876
|
+
findings: [{ level: "error", message: "Malformed gate: not an object." }],
|
|
877
|
+
structured: safeStructured
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
const spec = gateSpec;
|
|
881
|
+
if (!isGateKind(spec.kind)) {
|
|
882
|
+
return {
|
|
883
|
+
passed: false,
|
|
884
|
+
findings: [
|
|
885
|
+
{
|
|
886
|
+
level: "error",
|
|
887
|
+
message: `Malformed gate: unknown kind ${formatValue(spec.kind)}.`
|
|
912
888
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
return "unknown";
|
|
889
|
+
],
|
|
890
|
+
structured: safeStructured
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
if (spec.pendingEngine === true) {
|
|
894
|
+
return {
|
|
895
|
+
passed: true,
|
|
896
|
+
findings: [
|
|
897
|
+
{
|
|
898
|
+
level: "info",
|
|
899
|
+
message: `Gate "${spec.kind}" is advisory (pending engine); not enforced.`
|
|
925
900
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
901
|
+
],
|
|
902
|
+
structured: safeStructured
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
if (!isPlainObject(evidence)) {
|
|
906
|
+
return {
|
|
907
|
+
passed: false,
|
|
908
|
+
findings: [
|
|
909
|
+
{ level: "error", message: "Malformed evidence: not an object." }
|
|
910
|
+
],
|
|
911
|
+
structured: safeStructured
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
const result = evidence.result;
|
|
915
|
+
const resultIsKnown = result === "passed" || result === "failed" || result === "blocked";
|
|
916
|
+
const conditions = Array.isArray(spec.conditions) ? spec.conditions : null;
|
|
917
|
+
if (conditions === null) {
|
|
918
|
+
if (result === "passed") {
|
|
919
|
+
return {
|
|
920
|
+
passed: true,
|
|
921
|
+
findings: [
|
|
922
|
+
{
|
|
923
|
+
level: "info",
|
|
924
|
+
message: `Gate "${spec.kind}" passed on evidence result.`
|
|
925
|
+
}
|
|
926
|
+
],
|
|
927
|
+
structured: safeStructured
|
|
928
|
+
};
|
|
939
929
|
}
|
|
940
|
-
|
|
941
|
-
|
|
930
|
+
return {
|
|
931
|
+
passed: false,
|
|
932
|
+
findings: [
|
|
933
|
+
{
|
|
934
|
+
level: "error",
|
|
935
|
+
message: resultIsKnown ? `Gate "${spec.kind}" not satisfied: evidence result is "${String(result)}".` : `Gate "${spec.kind}" not satisfied: evidence result is missing or invalid.`
|
|
936
|
+
}
|
|
937
|
+
],
|
|
938
|
+
structured: safeStructured
|
|
939
|
+
};
|
|
942
940
|
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
return new URL(match[1]).href;
|
|
952
|
-
} catch {
|
|
953
|
-
return null;
|
|
941
|
+
const mode = spec.mode === "any" ? "any" : "all";
|
|
942
|
+
const findings = [];
|
|
943
|
+
const outcomes = [];
|
|
944
|
+
for (const raw of conditions) {
|
|
945
|
+
const { ok, finding } = evaluateCondition(raw, safeStructured);
|
|
946
|
+
outcomes.push(ok);
|
|
947
|
+
if (finding)
|
|
948
|
+
findings.push(finding);
|
|
954
949
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
return null;
|
|
962
|
-
return findExistingPr(branchName, cwd, provider) || null;
|
|
963
|
-
}
|
|
964
|
-
function remoteBranchExists(branchName, cwd) {
|
|
965
|
-
try {
|
|
966
|
-
execFileSync("git", ["ls-remote", "--exit-code", "origin", `refs/heads/${branchName}`], { cwd, stdio: "pipe" });
|
|
967
|
-
return true;
|
|
968
|
-
} catch {
|
|
969
|
-
return false;
|
|
950
|
+
if (result === "blocked") {
|
|
951
|
+
findings.unshift({
|
|
952
|
+
level: "error",
|
|
953
|
+
message: `Gate "${spec.kind}" cannot pass: evidence result is "blocked".`
|
|
954
|
+
});
|
|
955
|
+
return { passed: false, findings, structured: safeStructured };
|
|
970
956
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
cwd,
|
|
979
|
-
stdio: "pipe"
|
|
957
|
+
let predicatePassed;
|
|
958
|
+
if (mode === "any") {
|
|
959
|
+
predicatePassed = outcomes.length > 0 && outcomes.some((o) => o);
|
|
960
|
+
if (outcomes.length === 0) {
|
|
961
|
+
findings.push({
|
|
962
|
+
level: "error",
|
|
963
|
+
message: `Gate "${spec.kind}" (mode "any") has no conditions to satisfy.`
|
|
980
964
|
});
|
|
981
|
-
expectedSha = execFileSync("git", ["rev-parse", `refs/remotes/origin/${branchName}`], { cwd, encoding: "utf-8" }).trim();
|
|
982
|
-
} catch (err) {
|
|
983
|
-
log.warn(TAG2, `could not resolve remote tip for ${branchName}, falling back to weak lease: ${err instanceof Error ? err.message : err}`);
|
|
984
965
|
}
|
|
985
|
-
const lease = expectedSha ? `--force-with-lease=refs/heads/${branchName}:${expectedSha}` : "--force-with-lease";
|
|
986
|
-
execFileSync("git", ["push", lease, "-u", "origin", branchName], {
|
|
987
|
-
cwd,
|
|
988
|
-
stdio: "pipe"
|
|
989
|
-
});
|
|
990
966
|
} else {
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
967
|
+
predicatePassed = outcomes.every((o) => o);
|
|
968
|
+
}
|
|
969
|
+
if (predicatePassed) {
|
|
970
|
+
findings.push({
|
|
971
|
+
level: "info",
|
|
972
|
+
message: `Gate "${spec.kind}" predicate satisfied (mode "${mode}").`
|
|
994
973
|
});
|
|
995
974
|
}
|
|
975
|
+
return { passed: predicatePassed, findings, structured: safeStructured };
|
|
996
976
|
}
|
|
997
|
-
function
|
|
998
|
-
if (
|
|
999
|
-
return
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
}
|
|
1006
|
-
} catch (err) {
|
|
1007
|
-
throw new Error(`renameRemoteBranch: could not resolve HEAD: ${err instanceof Error ? err.message : err}`);
|
|
977
|
+
function evaluateCondition(raw, structured) {
|
|
978
|
+
if (!isPlainObject(raw)) {
|
|
979
|
+
return {
|
|
980
|
+
ok: false,
|
|
981
|
+
finding: {
|
|
982
|
+
level: "error",
|
|
983
|
+
message: "Malformed condition: not an object."
|
|
984
|
+
}
|
|
985
|
+
};
|
|
1008
986
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
987
|
+
const cond = raw;
|
|
988
|
+
if (typeof cond.path !== "string" || cond.path.length === 0) {
|
|
989
|
+
return {
|
|
990
|
+
ok: false,
|
|
991
|
+
finding: {
|
|
992
|
+
level: "error",
|
|
993
|
+
message: "Malformed condition: missing string `path`."
|
|
994
|
+
}
|
|
995
|
+
};
|
|
1018
996
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
997
|
+
if (!isGateOperator(cond.op)) {
|
|
998
|
+
return {
|
|
999
|
+
ok: false,
|
|
1000
|
+
finding: {
|
|
1001
|
+
level: "error",
|
|
1002
|
+
message: `Unknown operator ${formatValue(cond.op)} at "${cond.path}"; failing closed.`,
|
|
1003
|
+
path: cond.path
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
const actual = resolvePath(structured, cond.path);
|
|
1008
|
+
const expected = cond.value;
|
|
1009
|
+
const op = cond.op;
|
|
1010
|
+
let ok;
|
|
1011
|
+
switch (op) {
|
|
1012
|
+
case "exists":
|
|
1013
|
+
ok = actual !== undefined;
|
|
1014
|
+
break;
|
|
1015
|
+
case "eq":
|
|
1016
|
+
ok = strictEquals(actual, expected);
|
|
1017
|
+
break;
|
|
1018
|
+
case "neq":
|
|
1019
|
+
ok = !strictEquals(actual, expected);
|
|
1020
|
+
break;
|
|
1021
|
+
case "gte":
|
|
1022
|
+
case "gt":
|
|
1023
|
+
case "lte":
|
|
1024
|
+
case "lt":
|
|
1025
|
+
ok = numericCompare(op, actual, expected);
|
|
1026
|
+
break;
|
|
1027
|
+
case "contains":
|
|
1028
|
+
ok = containsCheck(actual, expected);
|
|
1029
|
+
break;
|
|
1030
|
+
default: {
|
|
1031
|
+
const _never = op;
|
|
1032
|
+
ok = false;
|
|
1047
1033
|
}
|
|
1048
|
-
return null;
|
|
1049
|
-
} catch {
|
|
1050
|
-
return null;
|
|
1051
1034
|
}
|
|
1035
|
+
if (ok)
|
|
1036
|
+
return { ok: true };
|
|
1037
|
+
return {
|
|
1038
|
+
ok: false,
|
|
1039
|
+
finding: {
|
|
1040
|
+
level: "error",
|
|
1041
|
+
message: `Condition failed: ${cond.path} ${op} ${formatValue(expected)} (actual: ${formatValue(actual)}).`,
|
|
1042
|
+
path: cond.path
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1052
1045
|
}
|
|
1053
|
-
function
|
|
1054
|
-
return
|
|
1055
|
-
"## Summary",
|
|
1056
|
-
"",
|
|
1057
|
-
`Automated PR for card **#${card.short_id} — ${card.title}**.`,
|
|
1058
|
-
"",
|
|
1059
|
-
"## Commits",
|
|
1060
|
-
"",
|
|
1061
|
-
"```",
|
|
1062
|
-
commitLog,
|
|
1063
|
-
"```",
|
|
1064
|
-
"",
|
|
1065
|
-
"## Card",
|
|
1066
|
-
"",
|
|
1067
|
-
card.description?.slice(0, 500) || "No description.",
|
|
1068
|
-
"",
|
|
1069
|
-
"---",
|
|
1070
|
-
"*Created by Harmony Agent Daemon*"
|
|
1071
|
-
].join(`
|
|
1072
|
-
`);
|
|
1046
|
+
function isPlainObject(value) {
|
|
1047
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1073
1048
|
}
|
|
1074
|
-
function
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
const existingUrl = findExistingPr(branchName, worktreePath, provider);
|
|
1085
|
-
if (existingUrl) {
|
|
1086
|
-
log.info(TAG2, `PR already exists for ${branchName}, updating body...`);
|
|
1087
|
-
updateExistingPr(branchName, body, worktreePath, provider);
|
|
1088
|
-
return existingUrl;
|
|
1089
|
-
}
|
|
1090
|
-
try {
|
|
1091
|
-
let result;
|
|
1092
|
-
switch (provider) {
|
|
1093
|
-
case "github":
|
|
1094
|
-
result = execFileSync("gh", ["pr", "create", "--title", title, "--body", body, "--base", base], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
1095
|
-
break;
|
|
1096
|
-
case "azure": {
|
|
1097
|
-
const azOutput = execFileSync("az", [
|
|
1098
|
-
"repos",
|
|
1099
|
-
"pr",
|
|
1100
|
-
"create",
|
|
1101
|
-
"--title",
|
|
1102
|
-
title,
|
|
1103
|
-
"--description",
|
|
1104
|
-
body,
|
|
1105
|
-
"--source-branch",
|
|
1106
|
-
branchName,
|
|
1107
|
-
"--target-branch",
|
|
1108
|
-
base,
|
|
1109
|
-
"--auto-complete",
|
|
1110
|
-
"false"
|
|
1111
|
-
], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
1112
|
-
try {
|
|
1113
|
-
const parsed = JSON.parse(azOutput);
|
|
1114
|
-
result = parsed.remoteUrl ?? parsed.url ?? azOutput;
|
|
1115
|
-
} catch {
|
|
1116
|
-
result = azOutput;
|
|
1117
|
-
}
|
|
1118
|
-
break;
|
|
1049
|
+
function resolvePath(root, path) {
|
|
1050
|
+
const segments = path.split(".");
|
|
1051
|
+
let current = root;
|
|
1052
|
+
for (const segment of segments) {
|
|
1053
|
+
if (current === null || current === undefined)
|
|
1054
|
+
return;
|
|
1055
|
+
if (Array.isArray(current)) {
|
|
1056
|
+
const index = Number(segment);
|
|
1057
|
+
if (!Number.isInteger(index) || index < 0 || index >= current.length) {
|
|
1058
|
+
return;
|
|
1119
1059
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
"--title",
|
|
1125
|
-
title,
|
|
1126
|
-
"--description",
|
|
1127
|
-
body,
|
|
1128
|
-
"--source-branch",
|
|
1129
|
-
branchName,
|
|
1130
|
-
"--target-branch",
|
|
1131
|
-
base,
|
|
1132
|
-
"--no-editor"
|
|
1133
|
-
], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
1134
|
-
break;
|
|
1135
|
-
default:
|
|
1136
|
-
log.warn(TAG2, `No PR CLI for provider "${provider}" — branch pushed but no PR created`);
|
|
1137
|
-
return null;
|
|
1138
|
-
}
|
|
1139
|
-
log.info(TAG2, `PR created: ${result}`);
|
|
1140
|
-
return result;
|
|
1141
|
-
} catch (err) {
|
|
1142
|
-
log.error(TAG2, `Failed to create PR: ${err instanceof Error ? err.message : err}`);
|
|
1143
|
-
return null;
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
function findExistingPr(branchName, worktreePath, provider) {
|
|
1147
|
-
try {
|
|
1148
|
-
switch (provider) {
|
|
1149
|
-
case "github":
|
|
1150
|
-
return execFileSync("gh", ["pr", "view", branchName, "--json", "url", "--jq", ".url"], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
1151
|
-
case "gitlab": {
|
|
1152
|
-
const json = execFileSync("glab", ["mr", "view", branchName, "--output", "json"], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
1153
|
-
const parsed = JSON.parse(json);
|
|
1154
|
-
return parsed.web_url || null;
|
|
1060
|
+
current = current[index];
|
|
1061
|
+
} else if (typeof current === "object") {
|
|
1062
|
+
if (!Object.hasOwn(current, segment)) {
|
|
1063
|
+
return;
|
|
1155
1064
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1065
|
+
current = current[segment];
|
|
1066
|
+
} else {
|
|
1067
|
+
return;
|
|
1158
1068
|
}
|
|
1159
|
-
} catch {
|
|
1160
|
-
return null;
|
|
1161
1069
|
}
|
|
1070
|
+
return current;
|
|
1162
1071
|
}
|
|
1163
|
-
function
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1072
|
+
function strictEquals(a, b) {
|
|
1073
|
+
if (a === null || b === null)
|
|
1074
|
+
return a === b;
|
|
1075
|
+
const t = typeof a;
|
|
1076
|
+
if (t !== "string" && t !== "number" && t !== "boolean")
|
|
1077
|
+
return false;
|
|
1078
|
+
return a === b;
|
|
1079
|
+
}
|
|
1080
|
+
function numericCompare(op, actual, expected) {
|
|
1081
|
+
if (typeof actual !== "number" || typeof expected !== "number")
|
|
1082
|
+
return false;
|
|
1083
|
+
if (Number.isNaN(actual) || Number.isNaN(expected))
|
|
1084
|
+
return false;
|
|
1085
|
+
switch (op) {
|
|
1086
|
+
case "gte":
|
|
1087
|
+
return actual >= expected;
|
|
1088
|
+
case "gt":
|
|
1089
|
+
return actual > expected;
|
|
1090
|
+
case "lte":
|
|
1091
|
+
return actual <= expected;
|
|
1092
|
+
case "lt":
|
|
1093
|
+
return actual < expected;
|
|
1179
1094
|
}
|
|
1180
1095
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
execFileAsync = promisify(execFile);
|
|
1185
|
-
VALID_PR_URL_RE = /^https:\/\/(github\.com|gitlab\.com|dev\.azure\.com|bitbucket\.org)\//;
|
|
1186
|
-
PR_URL_RE = /PR:\s*(https?:\/\/[^\s)]+)/;
|
|
1187
|
-
REVIEWED_SHA_RE = /^Reviewed-SHA:\s*([0-9a-f]{7,40})\s*$/im;
|
|
1188
|
-
});
|
|
1189
|
-
|
|
1190
|
-
// src/http-server.ts
|
|
1191
|
-
import {
|
|
1192
|
-
createServer
|
|
1193
|
-
} from "node:http";
|
|
1194
|
-
|
|
1195
|
-
class HttpServer {
|
|
1196
|
-
opts;
|
|
1197
|
-
server = null;
|
|
1198
|
-
boundPort = null;
|
|
1199
|
-
constructor(opts) {
|
|
1200
|
-
this.opts = opts;
|
|
1096
|
+
function containsCheck(actual, expected) {
|
|
1097
|
+
if (typeof actual === "string" && typeof expected === "string") {
|
|
1098
|
+
return actual.includes(expected);
|
|
1201
1099
|
}
|
|
1202
|
-
|
|
1203
|
-
return
|
|
1100
|
+
if (Array.isArray(actual)) {
|
|
1101
|
+
return actual.some((el) => strictEquals(el, expected));
|
|
1204
1102
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
const startPort = this.opts.port;
|
|
1217
|
-
for (let i = 0;i < attempts; i++) {
|
|
1218
|
-
const port = startPort + i;
|
|
1219
|
-
try {
|
|
1220
|
-
await this.listenOnce(port);
|
|
1221
|
-
this.boundPort = port;
|
|
1222
|
-
if (port !== startPort) {
|
|
1223
|
-
log.info(TAG3, `port ${startPort} busy — bound to ${port} instead`);
|
|
1224
|
-
}
|
|
1225
|
-
return port;
|
|
1226
|
-
} catch (err) {
|
|
1227
|
-
const lastAttempt = i === attempts - 1;
|
|
1228
|
-
if (isAddrInUse(err) && !lastAttempt) {
|
|
1229
|
-
log.debug(TAG3, `port ${port} in use, trying ${port + 1}`);
|
|
1230
|
-
continue;
|
|
1231
|
-
}
|
|
1232
|
-
throw err;
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
throw new Error("HTTP server failed to bind");
|
|
1236
|
-
}
|
|
1237
|
-
listenOnce(port) {
|
|
1238
|
-
return new Promise((resolve, reject) => {
|
|
1239
|
-
const server = this.server;
|
|
1240
|
-
if (!server) {
|
|
1241
|
-
reject(new Error("server not created"));
|
|
1242
|
-
return;
|
|
1243
|
-
}
|
|
1244
|
-
const onError = (err) => {
|
|
1245
|
-
server.removeListener("listening", onListening);
|
|
1246
|
-
reject(err);
|
|
1247
|
-
};
|
|
1248
|
-
const onListening = () => {
|
|
1249
|
-
server.removeListener("error", onError);
|
|
1250
|
-
resolve();
|
|
1251
|
-
};
|
|
1252
|
-
server.once("error", onError);
|
|
1253
|
-
server.once("listening", onListening);
|
|
1254
|
-
server.listen(port, this.opts.bindAddr);
|
|
1255
|
-
});
|
|
1256
|
-
}
|
|
1257
|
-
async stop() {
|
|
1258
|
-
if (!this.server)
|
|
1259
|
-
return;
|
|
1260
|
-
await new Promise((resolve) => {
|
|
1261
|
-
this.server?.close(() => resolve());
|
|
1262
|
-
});
|
|
1263
|
-
this.server = null;
|
|
1264
|
-
this.boundPort = null;
|
|
1265
|
-
}
|
|
1266
|
-
async route(req, res) {
|
|
1267
|
-
const url = new URL(req.url ?? "/", "http://localhost");
|
|
1268
|
-
const method = (req.method ?? "GET").toUpperCase();
|
|
1269
|
-
const path = url.pathname;
|
|
1270
|
-
if (method === "GET" && path === "/health") {
|
|
1271
|
-
return this.respondHealth(res);
|
|
1272
|
-
}
|
|
1273
|
-
if (method === "GET" && path === "/status") {
|
|
1274
|
-
return this.respondStatus(res);
|
|
1275
|
-
}
|
|
1276
|
-
if (method === "POST") {
|
|
1277
|
-
const cmd = parseCommand(path);
|
|
1278
|
-
if (cmd) {
|
|
1279
|
-
return this.respondCommand(res, cmd.command, cmd.cardId);
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
res.writeHead(404, { "content-type": "application/json" });
|
|
1283
|
-
res.end(JSON.stringify({ error: "not_found", path }));
|
|
1284
|
-
}
|
|
1285
|
-
respondHealth(res) {
|
|
1286
|
-
const health = this.opts.getHealth();
|
|
1287
|
-
res.writeHead(health.healthy ? 200 : 503, {
|
|
1288
|
-
"content-type": "application/json"
|
|
1289
|
-
});
|
|
1290
|
-
res.end(JSON.stringify(health));
|
|
1291
|
-
}
|
|
1292
|
-
respondStatus(res) {
|
|
1293
|
-
const snapshot = this.opts.getStatus();
|
|
1294
|
-
res.writeHead(200, { "content-type": "application/json" });
|
|
1295
|
-
res.end(JSON.stringify(snapshot));
|
|
1103
|
+
return false;
|
|
1104
|
+
}
|
|
1105
|
+
function formatValue(value) {
|
|
1106
|
+
if (value === undefined)
|
|
1107
|
+
return "undefined";
|
|
1108
|
+
if (value === null)
|
|
1109
|
+
return "null";
|
|
1110
|
+
if (typeof value === "string")
|
|
1111
|
+
return JSON.stringify(value);
|
|
1112
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
1113
|
+
return String(value);
|
|
1296
1114
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
res.end(JSON.stringify({ ok: true, command, cardId }));
|
|
1302
|
-
} catch (err) {
|
|
1303
|
-
res.writeHead(500, { "content-type": "application/json" });
|
|
1304
|
-
res.end(JSON.stringify({
|
|
1305
|
-
error: "command_failed",
|
|
1306
|
-
detail: err instanceof Error ? err.message : String(err)
|
|
1307
|
-
}));
|
|
1308
|
-
}
|
|
1115
|
+
try {
|
|
1116
|
+
return JSON.stringify(value);
|
|
1117
|
+
} catch {
|
|
1118
|
+
return "[unserializable]";
|
|
1309
1119
|
}
|
|
1310
1120
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1121
|
+
var GATE_KINDS, GATE_OPERATORS;
|
|
1122
|
+
var init_gateEvaluate = __esm(() => {
|
|
1123
|
+
GATE_KINDS = [
|
|
1124
|
+
"build_green",
|
|
1125
|
+
"review_passed",
|
|
1126
|
+
"checklist",
|
|
1127
|
+
"dod",
|
|
1128
|
+
"artifact",
|
|
1129
|
+
"label",
|
|
1130
|
+
"custom"
|
|
1131
|
+
];
|
|
1132
|
+
GATE_OPERATORS = [
|
|
1133
|
+
"eq",
|
|
1134
|
+
"neq",
|
|
1135
|
+
"gte",
|
|
1136
|
+
"gt",
|
|
1137
|
+
"lte",
|
|
1138
|
+
"lt",
|
|
1139
|
+
"contains",
|
|
1140
|
+
"exists"
|
|
1141
|
+
];
|
|
1323
1142
|
});
|
|
1324
1143
|
|
|
1325
|
-
//
|
|
1326
|
-
function
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1144
|
+
// ../harmony-shared/dist/gateEvidence.js
|
|
1145
|
+
function toStageGateEvidenceInsert(context, evidence) {
|
|
1146
|
+
return {
|
|
1147
|
+
card_id: context.cardId,
|
|
1148
|
+
workspace_id: context.workspaceId,
|
|
1149
|
+
stage_id: context.stageId,
|
|
1150
|
+
gate_kind: context.gate.kind,
|
|
1151
|
+
result: evidence.result,
|
|
1152
|
+
structured: evidence.structured
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// ../harmony-shared/dist/logger.js
|
|
1157
|
+
var init_logger = () => {};
|
|
1158
|
+
// ../harmony-shared/dist/playbookCatalog.js
|
|
1159
|
+
var init_playbookCatalog = () => {};
|
|
1160
|
+
|
|
1161
|
+
// ../harmony-shared/dist/playbookStage.js
|
|
1162
|
+
function normalizeLoopDef(raw) {
|
|
1163
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
1164
|
+
return null;
|
|
1165
|
+
const obj = raw;
|
|
1166
|
+
if (obj.mode !== "converge" && obj.mode !== "fanout")
|
|
1167
|
+
return null;
|
|
1168
|
+
const mode = obj.mode;
|
|
1169
|
+
const rawMax = obj.max_iterations;
|
|
1170
|
+
const maxInt = typeof rawMax === "number" && Number.isFinite(rawMax) && rawMax >= 1 ? Math.floor(rawMax) : DEFAULT_LOOP_MAX_ITERATIONS;
|
|
1171
|
+
const exitGate = obj.exit_gate && typeof obj.exit_gate === "object" && !Array.isArray(obj.exit_gate) ? obj.exit_gate : null;
|
|
1172
|
+
const def = { mode, max_iterations: maxInt };
|
|
1173
|
+
if (exitGate)
|
|
1174
|
+
def.exit_gate = exitGate;
|
|
1175
|
+
if (obj.item_source && typeof obj.item_source === "object" && !Array.isArray(obj.item_source)) {
|
|
1176
|
+
def.item_source = obj.item_source;
|
|
1335
1177
|
}
|
|
1336
|
-
if (
|
|
1337
|
-
|
|
1178
|
+
if (typeof obj.concurrency === "number" && obj.concurrency >= 1) {
|
|
1179
|
+
def.concurrency = Math.floor(obj.concurrency);
|
|
1338
1180
|
}
|
|
1339
|
-
|
|
1181
|
+
if (obj.on_item_fail === "continue" || obj.on_item_fail === "halt") {
|
|
1182
|
+
def.on_item_fail = obj.on_item_fail;
|
|
1183
|
+
}
|
|
1184
|
+
return def;
|
|
1340
1185
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
if (existing.includes("CI checks failed"))
|
|
1344
|
-
return;
|
|
1345
|
-
const sep = existing ? `
|
|
1346
|
-
` : "";
|
|
1347
|
-
const ts = new Date().toISOString();
|
|
1348
|
-
await client.updateCard(card.id, {
|
|
1349
|
-
description: `${existing}${sep}CI checks failed at ${ts}`
|
|
1350
|
-
});
|
|
1186
|
+
function getStageLoop(stage) {
|
|
1187
|
+
return normalizeLoopDef(stage.loop);
|
|
1351
1188
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
const obj = resolvedLabels.find((l) => l.name.toLowerCase() === name);
|
|
1355
|
-
if (obj)
|
|
1356
|
-
await client.removeLabelFromCard(card.id, obj.id);
|
|
1189
|
+
function isConvergeLoop(loop) {
|
|
1190
|
+
return loop !== null && loop.mode === "converge";
|
|
1357
1191
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
const
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
ciStatus,
|
|
1367
|
-
headSha,
|
|
1368
|
-
reviewedSha,
|
|
1369
|
-
config: autoMerge
|
|
1370
|
-
});
|
|
1371
|
-
switch (action) {
|
|
1372
|
-
case "wait":
|
|
1373
|
-
log.debug(TAG4, `#${card.short_id} waiting (ci=${ciStatus})`);
|
|
1374
|
-
return;
|
|
1375
|
-
case "stamp-failure":
|
|
1376
|
-
log.info(TAG4, `#${card.short_id} CI failed — flagging for human`);
|
|
1377
|
-
await stampCiFailure(client, card);
|
|
1378
|
-
return;
|
|
1379
|
-
case "rereview":
|
|
1380
|
-
log.info(TAG4, `#${card.short_id} branch changed since review — re-reviewing`);
|
|
1381
|
-
await removeApprovedLabel(client, card, resolvedLabels, config.review.approvedLabel);
|
|
1382
|
-
return;
|
|
1383
|
-
case "merge":
|
|
1384
|
-
log.info(TAG4, `#${card.short_id} auto-merging (${autoMerge.strategy})`);
|
|
1385
|
-
await mergePullRequest(prUrl, cwd, provider, autoMerge.strategy, autoMerge.deleteBranch);
|
|
1386
|
-
return;
|
|
1192
|
+
function resolveLoopExitGate(stage, loop) {
|
|
1193
|
+
return loop.exit_gate ?? stage.gate ?? null;
|
|
1194
|
+
}
|
|
1195
|
+
function decideLoopContinuation(args) {
|
|
1196
|
+
const { loop, gatePassed, hasExitGate, completedIterations } = args;
|
|
1197
|
+
const max = Math.max(1, Math.floor(loop.max_iterations) || 1);
|
|
1198
|
+
if (!hasExitGate) {
|
|
1199
|
+
return completedIterations >= max ? "exit" : "iterate";
|
|
1387
1200
|
}
|
|
1201
|
+
if (gatePassed)
|
|
1202
|
+
return "exit";
|
|
1203
|
+
if (completedIterations >= max)
|
|
1204
|
+
return "exhausted";
|
|
1205
|
+
return "iterate";
|
|
1388
1206
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
});
|
|
1394
|
-
// ../harmony-shared/dist/branchRef.js
|
|
1395
|
-
var BRANCH_REF_PATTERN, SAFE_GIT_REF_PATTERN;
|
|
1396
|
-
var init_branchRef = __esm(() => {
|
|
1397
|
-
BRANCH_REF_PATTERN = /Branch:\s*`([^`]+)`/;
|
|
1398
|
-
SAFE_GIT_REF_PATTERN = /^[a-zA-Z0-9/_.-]+$/;
|
|
1399
|
-
});
|
|
1400
|
-
|
|
1401
|
-
// ../harmony-shared/dist/cardLinks.js
|
|
1402
|
-
var init_cardLinks = () => {};
|
|
1403
|
-
// ../harmony-shared/dist/classification.js
|
|
1404
|
-
function escalateTier(tier) {
|
|
1405
|
-
const i = MODEL_TIERS.indexOf(tier);
|
|
1406
|
-
return MODEL_TIERS[Math.min(i + 1, MODEL_TIERS.length - 1)];
|
|
1207
|
+
function readStageDefs(def) {
|
|
1208
|
+
if (def.steps_version !== 2)
|
|
1209
|
+
return [];
|
|
1210
|
+
return Array.isArray(def.steps) ? def.steps : [];
|
|
1407
1211
|
}
|
|
1408
|
-
function
|
|
1409
|
-
|
|
1212
|
+
function resolveStageDef(def, currentStage) {
|
|
1213
|
+
if (def.steps_version !== 2)
|
|
1214
|
+
return { kind: "not_stage_model" };
|
|
1215
|
+
const stages = readStageDefs(def);
|
|
1216
|
+
const index = stages.findIndex((s) => s?.id === currentStage);
|
|
1217
|
+
if (index === -1)
|
|
1218
|
+
return { kind: "stage_not_found" };
|
|
1219
|
+
return { kind: "found", stage: stages[index], index };
|
|
1410
1220
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
MODEL_TIERS = ["simple", "advanced", "research"];
|
|
1414
|
-
});
|
|
1415
|
-
|
|
1416
|
-
// ../harmony-shared/dist/commentSerializer.js
|
|
1417
|
-
function sanitizeHeaderField(value) {
|
|
1418
|
-
return value.replace(/[\]\r\n|<>]/g, " ").trim() || "—";
|
|
1221
|
+
function isAgentRunnableOwner(owner) {
|
|
1222
|
+
return owner === "agent" || owner === "either";
|
|
1419
1223
|
}
|
|
1420
|
-
function
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1224
|
+
function nextStageAfter(def, index) {
|
|
1225
|
+
const stages = readStageDefs(def);
|
|
1226
|
+
if (index < 0 || index >= stages.length)
|
|
1227
|
+
return { kind: "out_of_range" };
|
|
1228
|
+
const next = stages[index + 1];
|
|
1229
|
+
if (!next)
|
|
1230
|
+
return { kind: "terminal" };
|
|
1231
|
+
return { kind: "next", stage: next, index: index + 1 };
|
|
1425
1232
|
}
|
|
1426
|
-
function
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
keep.add(c.id);
|
|
1437
|
-
keep.add(c.confirms_id);
|
|
1233
|
+
function entryActionAllowlist(entryAction) {
|
|
1234
|
+
if (!entryAction)
|
|
1235
|
+
return null;
|
|
1236
|
+
const direct = SKILL_TOOL_ALLOWLIST[entryAction];
|
|
1237
|
+
if (direct)
|
|
1238
|
+
return direct;
|
|
1239
|
+
if (HARMONY_TOOL_RE.test(entryAction)) {
|
|
1240
|
+
const qualified = `mcp__harmony__${entryAction}`;
|
|
1241
|
+
if (STAGE_DAEMON_OWNED_TOOLS.includes(qualified)) {
|
|
1242
|
+
return null;
|
|
1438
1243
|
}
|
|
1244
|
+
return qualified;
|
|
1439
1245
|
}
|
|
1440
|
-
return
|
|
1246
|
+
return null;
|
|
1441
1247
|
}
|
|
1442
|
-
function
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
const keep = criticalIds(visible);
|
|
1455
|
-
const recentThreshold = visible.length - maxComments;
|
|
1456
|
-
rendered = visible.filter((c, i) => i >= recentThreshold || keep.has(c.id));
|
|
1457
|
-
elidedCount = visible.length - rendered.length;
|
|
1458
|
-
}
|
|
1459
|
-
const ref = (id) => {
|
|
1460
|
-
const n = indexById.get(id);
|
|
1461
|
-
return n ? `#${n}` : `#${id.slice(0, 8)}`;
|
|
1248
|
+
function stageDisallowedTools() {
|
|
1249
|
+
return STAGE_DAEMON_OWNED_TOOLS.length > 0 ? STAGE_DAEMON_OWNED_TOOLS.join(",") : null;
|
|
1250
|
+
}
|
|
1251
|
+
var DEFAULT_LOOP_MAX_ITERATIONS = 5, SKILL_TOOL_ALLOWLIST, HARMONY_TOOL_RE, STAGE_DAEMON_OWNED_TOOLS;
|
|
1252
|
+
var init_playbookStage = __esm(() => {
|
|
1253
|
+
SKILL_TOOL_ALLOWLIST = {
|
|
1254
|
+
hmy: "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*",
|
|
1255
|
+
"hmy-new": "Read,Grep,Glob,mcp__harmony__*",
|
|
1256
|
+
"hmy-plan": "Read,Grep,Glob,mcp__harmony__*",
|
|
1257
|
+
"hmy-review": "Read,Grep,Glob,Bash,mcp__harmony__*",
|
|
1258
|
+
"hmy-cleanup": "Read,Grep,Glob,mcp__harmony__*",
|
|
1259
|
+
"hmy-standup": "Read,Grep,Glob,mcp__harmony__*"
|
|
1462
1260
|
};
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
for (const c of rendered) {
|
|
1471
|
-
const tags = [];
|
|
1472
|
-
if (c.edited_at)
|
|
1473
|
-
tags.push("edited");
|
|
1474
|
-
if (c.reply_to_id)
|
|
1475
|
-
tags.push(`reply to ${ref(c.reply_to_id)}`);
|
|
1476
|
-
if (c.supersedes_id)
|
|
1477
|
-
tags.push(`supersedes ${ref(c.supersedes_id)}`);
|
|
1478
|
-
if (c.confirms_id)
|
|
1479
|
-
tags.push(`confirms ${ref(c.confirms_id)}`);
|
|
1480
|
-
if (c.resolved_at)
|
|
1481
|
-
tags.push("resolved");
|
|
1482
|
-
const tagStr = tags.length ? ` | ${tags.join(" | ")}` : "";
|
|
1483
|
-
const header = `[${sanitizeHeaderField(ref(c.id))} | ${sanitizeHeaderField(c.author_type)} | ${authorLabel(c)} | ${sanitizeHeaderField(c.comment_type)} | ${sanitizeHeaderField(c.created_at)}${tagStr}]`;
|
|
1484
|
-
const fencedBody = c.body.trim().replaceAll("<", "<").replaceAll(">", ">");
|
|
1485
|
-
lines.push({
|
|
1486
|
-
at: c.created_at,
|
|
1487
|
-
text: `${header}
|
|
1488
|
-
<comment-body>
|
|
1489
|
-
${fencedBody}
|
|
1490
|
-
</comment-body>`
|
|
1491
|
-
});
|
|
1492
|
-
}
|
|
1493
|
-
for (const a of activity) {
|
|
1494
|
-
const actor = a.actor ? `${a.actor} ` : "";
|
|
1495
|
-
lines.push({ at: a.at, text: `· (system) ${a.at} — ${actor}${a.text}` });
|
|
1496
|
-
}
|
|
1497
|
-
lines.sort((a, b) => a.at.localeCompare(b.at));
|
|
1498
|
-
const body = lines.map((l) => l.text).join(`
|
|
1261
|
+
HARMONY_TOOL_RE = /^harmony_[a-z_]+$/;
|
|
1262
|
+
STAGE_DAEMON_OWNED_TOOLS = [
|
|
1263
|
+
"mcp__harmony__harmony_end_agent_session",
|
|
1264
|
+
"mcp__harmony__harmony_start_agent_session",
|
|
1265
|
+
"mcp__harmony__harmony_move_card"
|
|
1266
|
+
];
|
|
1267
|
+
});
|
|
1499
1268
|
|
|
1500
|
-
|
|
1501
|
-
|
|
1269
|
+
// ../harmony-shared/dist/projectTemplates.js
|
|
1270
|
+
var init_projectTemplates = () => {};
|
|
1502
1271
|
|
|
1503
|
-
|
|
1504
|
-
|
|
1272
|
+
// ../harmony-shared/dist/reviewMethodology.js
|
|
1273
|
+
var REVIEW_SYSTEM_PROMPT = `You are a senior code reviewer. Follow this two-pass methodology strictly.
|
|
1274
|
+
Report findings; do NOT fix them. This is a read-only review.
|
|
1505
1275
|
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1276
|
+
Review the diff through five lenses on every pass: functionality, security,
|
|
1277
|
+
performance, code quality, and best practices. For every finding, set
|
|
1278
|
+
\`relatedToDiff\`: true when the change under review introduced or exposed it,
|
|
1279
|
+
false when it is a pre-existing issue you happened to notice. Only diff-caused
|
|
1280
|
+
findings gate the verdict — pre-existing ones are reported for context and never
|
|
1281
|
+
block.
|
|
1512
1282
|
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1283
|
+
## Two-Pass Review
|
|
1284
|
+
|
|
1285
|
+
### Pass 1 — CRITICAL (highest severity)
|
|
1286
|
+
|
|
1287
|
+
**SQL & Data Safety**
|
|
1288
|
+
- String interpolation in SQL — use parameterized queries / prepared statements
|
|
1289
|
+
- TOCTOU races: check-then-set patterns that should be atomic WHERE + UPDATE
|
|
1290
|
+
|
|
1291
|
+
**Race Conditions & Concurrency**
|
|
1292
|
+
- Read-check-write without uniqueness constraint or duplicate key handling
|
|
1293
|
+
- Status transitions without atomic WHERE old_status UPDATE SET new_status
|
|
1294
|
+
- Unsafe HTML rendering (dangerouslySetInnerHTML, v-html) on user-controlled data (XSS)
|
|
1295
|
+
|
|
1296
|
+
**Security & Access Control**
|
|
1297
|
+
- Hardcoded secrets, API keys, or credentials committed to source
|
|
1298
|
+
- New endpoints, mutations, or service-role/RLS-exempt queries missing an auth or ownership check
|
|
1299
|
+
- Over-broad CORS, missing input validation on a trust boundary, injection beyond SQL (command, path, template)
|
|
1300
|
+
|
|
1301
|
+
**LLM Output Trust Boundary**
|
|
1302
|
+
- LLM-generated values written to DB without format validation (EMAIL_REGEXP, URI.parse, .trim())
|
|
1303
|
+
- Structured tool output accepted without type/shape checks before database writes
|
|
1304
|
+
|
|
1305
|
+
**Enum & Value Completeness**
|
|
1306
|
+
- When the diff introduces a new enum/status/type value, trace it through every consumer
|
|
1307
|
+
- Check allowlists, filter arrays, and case/if-elsif chains for the new value
|
|
1308
|
+
- Use Grep to find all references to sibling values and Read each match — look OUTSIDE the diff
|
|
1309
|
+
|
|
1310
|
+
### Pass 2 — INFORMATIONAL (lower severity)
|
|
1311
|
+
|
|
1312
|
+
**Functionality & Edge Cases**
|
|
1313
|
+
- Logic errors, off-by-one, unhandled null/undefined, wrong API or library usage
|
|
1314
|
+
- Conditional side effects: code paths that branch but forget a side effect on one branch (e.g., promoting without attaching URL)
|
|
1315
|
+
|
|
1316
|
+
**Performance**
|
|
1317
|
+
- O(n²) algorithms and O(n*m) lookups (Array.find in a loop instead of a Map/index)
|
|
1318
|
+
- N+1 queries, unbounded fetches missing pagination, repeated work that should be cached/memoized
|
|
1319
|
+
- Unnecessary React re-renders (unstable props/deps, inline object/array literals); leaked subscriptions, timers, or listeners
|
|
1320
|
+
- Inline styles re-parsed every render
|
|
1321
|
+
|
|
1322
|
+
**Code Quality**
|
|
1323
|
+
- Dead code: variables assigned but never read, unreachable branches
|
|
1324
|
+
- Duplication that should be extracted, over-long functions, unclear naming
|
|
1325
|
+
- \`any\` / unchecked casts that defeat the type system
|
|
1326
|
+
- Comments/docstrings describing old behavior after code changed
|
|
1327
|
+
|
|
1328
|
+
**Best Practices & Conventions**
|
|
1329
|
+
- Deviations from established project conventions and framework idioms / anti-patterns
|
|
1330
|
+
- React hook dependency arrays that are wrong, missing, or over-broad
|
|
1331
|
+
- Accessibility gaps on new UI: missing labels, roles, alt text, or keyboard paths
|
|
1332
|
+
|
|
1333
|
+
**Test Gaps**
|
|
1334
|
+
- Missing negative-path tests for new error handling
|
|
1335
|
+
- Security enforcement features without integration tests
|
|
1336
|
+
|
|
1337
|
+
**Completeness Gaps**
|
|
1338
|
+
- Partial enum handling, incomplete error paths, missing edge cases that are straightforward to add
|
|
1339
|
+
|
|
1340
|
+
## Severity Classification
|
|
1341
|
+
|
|
1342
|
+
- **critical**: SQL safety, race conditions, XSS, secrets/auth/injection holes, LLM trust boundary violations, enum completeness gaps causing runtime errors
|
|
1343
|
+
- **major**: Missing requirements, broken functionality, significant completeness gaps, conditional side effects, performance regressions on a hot path
|
|
1344
|
+
- **minor**: Dead code, stale comments, test gaps, naming/duplication, minor view issues, cosmetic completeness gaps
|
|
1345
|
+
|
|
1346
|
+
## Suppressions — DO NOT flag these
|
|
1347
|
+
|
|
1348
|
+
- Redundancy that aids readability (e.g., present? redundant with length > 20)
|
|
1349
|
+
- "Add a comment explaining why this threshold was chosen" — thresholds change, comments rot
|
|
1350
|
+
- Consistency-only changes (wrapping a value to match how another constant is guarded)
|
|
1351
|
+
- Regex edge cases when input is constrained and the edge case never occurs in practice
|
|
1352
|
+
- Eval threshold changes — these are tuned empirically
|
|
1353
|
+
- Harmless no-ops (e.g., .reject on an element never in the array)
|
|
1354
|
+
- Pre-existing issues unrelated to the diff, beyond a single noted finding (set relatedToDiff:false; never block on them)
|
|
1355
|
+
- ANYTHING already addressed in the diff you are reviewing — read the FULL diff before flagging`, REVIEW_ACCEPTANCE_CHECKS = `## Acceptance Checks
|
|
1356
|
+
|
|
1357
|
+
Before judging code quality, verify the change actually satisfies the card.
|
|
1358
|
+
Derive one acceptance check per concrete requirement in the card description and
|
|
1359
|
+
one per subtask (the stated acceptance criteria). For each, assign a status from
|
|
1360
|
+
hard evidence — cite the file:line you read or the dev-server behaviour you
|
|
1361
|
+
observed that proves it:
|
|
1362
|
+
|
|
1363
|
+
- **pass** — implemented and verified by code you read or behaviour you observed
|
|
1364
|
+
- **partial** — started but incomplete (a missing branch, an edge case, or one of several bundled requirements)
|
|
1365
|
+
- **fail** — required but absent, or implemented incorrectly
|
|
1366
|
+
- **unverifiable** — cannot be confirmed from the diff or a running app (state why)
|
|
1367
|
+
|
|
1368
|
+
Do NOT mark a check "pass" on the implementing agent's say-so or a subtask's
|
|
1369
|
+
checkbox alone — only on evidence you found yourself. Any \`fail\` or \`partial\`
|
|
1370
|
+
check is an unaddressed requirement and forces a rejected verdict.`, QA_VISUAL_CHECKLIST = `## Visual QA Checklist
|
|
1371
|
+
|
|
1372
|
+
For each page affected by the changes:
|
|
1373
|
+
|
|
1374
|
+
1. **Visual scan** — Screenshot the page. Check for layout breaks, broken images, alignment issues, z-index problems.
|
|
1375
|
+
2. **Interactive elements** — Click every button, link, and control. Does each do what it says?
|
|
1376
|
+
3. **Forms** — Fill and submit. Test empty submission, invalid data, edge cases.
|
|
1377
|
+
4. **Navigation** — Check all paths in/out. Breadcrumbs, back button, deep links.
|
|
1378
|
+
5. **States** — Check empty state, loading state, error state, overflow state.
|
|
1379
|
+
6. **Console** — Check for JS exceptions, failed network requests (4xx/5xx), CORS errors after interactions.
|
|
1380
|
+
7. **Responsiveness** — If the change is visual, check mobile viewport (375px).
|
|
1381
|
+
|
|
1382
|
+
### SPA-Specific (React/Vite)
|
|
1383
|
+
- Use snapshot for navigation — client-side routes may not appear in link lists.
|
|
1384
|
+
- Check for stale state: navigate away and back — does data refresh correctly?
|
|
1385
|
+
- Test browser back/forward — does the app handle history correctly?
|
|
1386
|
+
- Watch for hydration errors or layout shifts after dynamic content loads.`, REVIEW_VERDICT_SCHEMA = `{
|
|
1387
|
+
"verdict": "approved" | "rejected",
|
|
1388
|
+
"summary": "Brief overall assessment",
|
|
1389
|
+
"scopeCheck": {
|
|
1390
|
+
"status": "clean" | "drift" | "missing",
|
|
1391
|
+
"notes": "Optional explanation of scope issues"
|
|
1392
|
+
},
|
|
1393
|
+
"acceptanceChecks": [
|
|
1394
|
+
{
|
|
1395
|
+
"criterion": "The requirement or subtask being verified",
|
|
1396
|
+
"status": "pass" | "partial" | "fail" | "unverifiable",
|
|
1397
|
+
"evidence": "file:line or observed behaviour that proves the status"
|
|
1398
|
+
}
|
|
1399
|
+
],
|
|
1400
|
+
"findings": [
|
|
1401
|
+
{
|
|
1402
|
+
"severity": "critical" | "major" | "minor",
|
|
1403
|
+
"category": "sql-safety | race-condition | security | llm-trust | enum-completeness | functional | performance | code-quality | best-practices | accessibility | visual | ux | console | scope | other",
|
|
1404
|
+
"title": "Short title",
|
|
1405
|
+
"description": "Detailed description of the issue",
|
|
1406
|
+
"location": "file:line (if applicable)",
|
|
1407
|
+
"relatedToDiff": true
|
|
1408
|
+
}
|
|
1409
|
+
]
|
|
1410
|
+
}`, REVIEW_DECISION_RULES = `Counting only findings with \`relatedToDiff: true\`:
|
|
1411
|
+
- **rejected**: Any acceptance check that is \`fail\` or \`partial\`, any \`critical\` finding, unaddressed requirements, or 2+ \`major\` findings.
|
|
1412
|
+
- **approved**: Every acceptance check \`pass\` (or \`unverifiable\` with a stated reason), no critical findings, at most 1 major finding; minor findings OK.`;
|
|
1413
|
+
// ../harmony-shared/dist/stageHandoff.js
|
|
1414
|
+
function buildHandoffCommentBody(input) {
|
|
1415
|
+
const handoff = {
|
|
1416
|
+
version: STAGE_HANDOFF_VERSION,
|
|
1417
|
+
stageId: input.stageId,
|
|
1418
|
+
stageName: input.stageName,
|
|
1419
|
+
artifactType: input.artifactType,
|
|
1420
|
+
produced: input.produced,
|
|
1421
|
+
decisions: input.decisions,
|
|
1422
|
+
nextStageNeeds: input.nextStageNeeds,
|
|
1423
|
+
producedAt: input.producedAt ?? new Date().toISOString()
|
|
1424
|
+
};
|
|
1425
|
+
const decisionLines = handoff.decisions.length > 0 ? handoff.decisions.map((d) => `- ${d}`).join(`
|
|
1426
|
+
`) : "_None._";
|
|
1427
|
+
const prose = [
|
|
1428
|
+
`**Stage handoff — ${handoff.stageName}**`,
|
|
1429
|
+
"",
|
|
1430
|
+
`**Produced:** ${handoff.produced}`,
|
|
1431
|
+
"",
|
|
1432
|
+
"**Decisions (settled — do not re-litigate):**",
|
|
1433
|
+
decisionLines,
|
|
1434
|
+
"",
|
|
1435
|
+
`**What the next stage needs:** ${handoff.nextStageNeeds}`
|
|
1436
|
+
].join(`
|
|
1437
|
+
`);
|
|
1438
|
+
const payload = [
|
|
1439
|
+
"```json",
|
|
1440
|
+
`// ${HANDOFF_MARKER}`,
|
|
1441
|
+
JSON.stringify(handoff, null, 2),
|
|
1442
|
+
"```"
|
|
1443
|
+
].join(`
|
|
1444
|
+
`);
|
|
1445
|
+
return `${prose}
|
|
1446
|
+
|
|
1447
|
+
${payload}`;
|
|
1448
|
+
}
|
|
1449
|
+
function isTypedStageHandoff(value) {
|
|
1450
|
+
if (typeof value !== "object" || value === null)
|
|
1451
|
+
return false;
|
|
1452
|
+
const v = value;
|
|
1453
|
+
return typeof v.stageId === "string" && typeof v.stageName === "string" && typeof v.produced === "string" && typeof v.nextStageNeeds === "string" && typeof v.producedAt === "string" && Array.isArray(v.decisions) && v.decisions.every((d) => typeof d === "string") && (v.artifactType === null || typeof v.artifactType === "string") && v.version === STAGE_HANDOFF_VERSION;
|
|
1454
|
+
}
|
|
1455
|
+
function parseHandoffCommentBody(body) {
|
|
1456
|
+
const match = HANDOFF_BLOCK_RE.exec(body);
|
|
1457
|
+
if (!match)
|
|
1458
|
+
return null;
|
|
1459
|
+
try {
|
|
1460
|
+
const parsed = JSON.parse(match[1]);
|
|
1461
|
+
return isTypedStageHandoff(parsed) ? parsed : null;
|
|
1462
|
+
} catch {
|
|
1463
|
+
return null;
|
|
1601
1464
|
}
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
for (const
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
if (
|
|
1609
|
-
|
|
1465
|
+
}
|
|
1466
|
+
function extractLatestHandoff(comments, opts = {}) {
|
|
1467
|
+
let best = null;
|
|
1468
|
+
for (const c of comments) {
|
|
1469
|
+
if (c.deleted_at)
|
|
1470
|
+
continue;
|
|
1471
|
+
if (c.author_type !== "agent")
|
|
1472
|
+
continue;
|
|
1473
|
+
const handoff = parseHandoffCommentBody(c.body);
|
|
1474
|
+
if (!handoff)
|
|
1475
|
+
continue;
|
|
1476
|
+
if (opts.excludeStageId && handoff.stageId === opts.excludeStageId)
|
|
1477
|
+
continue;
|
|
1478
|
+
if (!best || c.created_at.localeCompare(best.at) > 0) {
|
|
1479
|
+
best = { handoff, at: c.created_at };
|
|
1480
|
+
}
|
|
1610
1481
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1482
|
+
return best?.handoff ?? null;
|
|
1483
|
+
}
|
|
1484
|
+
function renderInheritedHandoffSection(handoff) {
|
|
1485
|
+
const decisions = handoff.decisions.length > 0 ? handoff.decisions.map((d) => `- ${d}`).join(`
|
|
1486
|
+
`) : "- (none recorded)";
|
|
1487
|
+
return [
|
|
1488
|
+
"## Inherited handoff (from the previous stage)",
|
|
1489
|
+
"",
|
|
1490
|
+
`This is the only state you inherit. The **${handoff.stageName}** stage produced it; treat its decisions as settled.`,
|
|
1491
|
+
"",
|
|
1492
|
+
`**Produced:** ${handoff.produced}`,
|
|
1493
|
+
"",
|
|
1494
|
+
"**Decisions you must respect:**",
|
|
1495
|
+
decisions,
|
|
1496
|
+
"",
|
|
1497
|
+
`**What you need to do with it:** ${handoff.nextStageNeeds}`
|
|
1498
|
+
].join(`
|
|
1499
|
+
`);
|
|
1500
|
+
}
|
|
1501
|
+
var STAGE_HANDOFF_VERSION = 1, HANDOFF_MARKER = "harmony:stage-handoff", HANDOFF_BLOCK_RE;
|
|
1502
|
+
var init_stageHandoff = __esm(() => {
|
|
1503
|
+
HANDOFF_BLOCK_RE = new RegExp("```json\\s*\\n//\\s*" + HANDOFF_MARKER + "\\s*\\n([\\s\\S]*?)\\n```", "m");
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
// ../harmony-shared/dist/types.js
|
|
1507
|
+
var init_types2 = () => {};
|
|
1508
|
+
|
|
1509
|
+
// ../harmony-shared/dist/index.js
|
|
1510
|
+
var init_dist = __esm(() => {
|
|
1511
|
+
init_branchRef();
|
|
1512
|
+
init_cardLinks();
|
|
1513
|
+
init_classification();
|
|
1514
|
+
init_commentSerializer();
|
|
1515
|
+
init_constants();
|
|
1516
|
+
init_gateEvaluate();
|
|
1517
|
+
init_logger();
|
|
1518
|
+
init_playbookCatalog();
|
|
1519
|
+
init_playbookStage();
|
|
1520
|
+
init_projectTemplates();
|
|
1521
|
+
init_stageHandoff();
|
|
1522
|
+
init_types2();
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
// src/git-pr.ts
|
|
1526
|
+
var exports_git_pr = {};
|
|
1527
|
+
__export(exports_git_pr, {
|
|
1528
|
+
validateGitProviderCli: () => validateGitProviderCli,
|
|
1529
|
+
upsertReviewedSha: () => upsertReviewedSha,
|
|
1530
|
+
updateExistingPr: () => updateExistingPr,
|
|
1531
|
+
resolvePrUrl: () => resolvePrUrl,
|
|
1532
|
+
resolvePrHeadBranch: () => resolvePrHeadBranch,
|
|
1533
|
+
renameRemoteBranch: () => renameRemoteBranch,
|
|
1534
|
+
remoteBranchExists: () => remoteBranchExists,
|
|
1535
|
+
pushBranch: () => pushBranch,
|
|
1536
|
+
mergePullRequest: () => mergePullRequest,
|
|
1537
|
+
getPrStatus: () => getPrStatus,
|
|
1538
|
+
getHeadSha: () => getHeadSha,
|
|
1539
|
+
getBranchWebUrl: () => getBranchWebUrl,
|
|
1540
|
+
findExistingPr: () => findExistingPr,
|
|
1541
|
+
extractReviewedSha: () => extractReviewedSha,
|
|
1542
|
+
extractPrUrl: () => extractPrUrl,
|
|
1543
|
+
detectGitProvider: () => detectGitProvider,
|
|
1544
|
+
deriveCiStatus: () => deriveCiStatus,
|
|
1545
|
+
decidePrBranch: () => decidePrBranch,
|
|
1546
|
+
createPullRequest: () => createPullRequest,
|
|
1547
|
+
checkPrMergeStatus: () => checkPrMergeStatus,
|
|
1548
|
+
buildPrBody: () => buildPrBody
|
|
1549
|
+
});
|
|
1550
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
1551
|
+
import { promisify } from "node:util";
|
|
1552
|
+
function detectGitProvider(cwd) {
|
|
1553
|
+
try {
|
|
1554
|
+
const url = execFileSync("git", ["remote", "get-url", "origin"], {
|
|
1555
|
+
cwd,
|
|
1556
|
+
encoding: "utf-8"
|
|
1557
|
+
}).trim();
|
|
1558
|
+
if (url.includes("github.com"))
|
|
1559
|
+
return "github";
|
|
1560
|
+
if (url.includes("dev.azure.com") || url.includes("visualstudio.com"))
|
|
1561
|
+
return "azure";
|
|
1562
|
+
if (url.includes("gitlab.com") || /\bgitlab\b/.test(url))
|
|
1563
|
+
return "gitlab";
|
|
1564
|
+
if (url.includes("bitbucket.org"))
|
|
1565
|
+
return "bitbucket";
|
|
1566
|
+
return "unknown";
|
|
1567
|
+
} catch {
|
|
1568
|
+
return "unknown";
|
|
1617
1569
|
}
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1570
|
+
}
|
|
1571
|
+
function validateGitProviderCli(provider, cwd) {
|
|
1572
|
+
switch (provider) {
|
|
1573
|
+
case "github": {
|
|
1574
|
+
try {
|
|
1575
|
+
execFileSync("gh", ["auth", "status"], { cwd, stdio: "pipe" });
|
|
1576
|
+
} catch {
|
|
1577
|
+
throw new Error("GitHub CLI (gh) is not authenticated. Run: gh auth login");
|
|
1578
|
+
}
|
|
1579
|
+
break;
|
|
1580
|
+
}
|
|
1581
|
+
case "azure": {
|
|
1582
|
+
try {
|
|
1583
|
+
execFileSync("az", ["--version"], { cwd, stdio: "pipe" });
|
|
1584
|
+
} catch {
|
|
1585
|
+
throw new Error("Azure CLI (az) not found. Install it: https://learn.microsoft.com/en-us/cli/azure/install-azure-cli");
|
|
1586
|
+
}
|
|
1587
|
+
try {
|
|
1588
|
+
execFileSync("az", ["account", "show"], { cwd, stdio: "pipe" });
|
|
1589
|
+
} catch {
|
|
1590
|
+
throw new Error("Azure CLI is not authenticated. Run: az login");
|
|
1591
|
+
}
|
|
1592
|
+
break;
|
|
1593
|
+
}
|
|
1594
|
+
case "gitlab": {
|
|
1595
|
+
try {
|
|
1596
|
+
execFileSync("glab", ["auth", "status"], { cwd, stdio: "pipe" });
|
|
1597
|
+
} catch {
|
|
1598
|
+
throw new Error("GitLab CLI (glab) is not installed or not authenticated. Install: https://gitlab.com/gitlab-org/cli — then run: glab auth login");
|
|
1599
|
+
}
|
|
1600
|
+
break;
|
|
1601
|
+
}
|
|
1602
|
+
case "bitbucket":
|
|
1603
|
+
case "unknown":
|
|
1604
|
+
log.warn(TAG2, `Git provider "${provider}" — PR creation will be skipped (no CLI support)`);
|
|
1605
|
+
break;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
function isValidPrUrl(url) {
|
|
1609
|
+
return VALID_PR_URL_RE.test(url);
|
|
1610
|
+
}
|
|
1611
|
+
function extractReviewedSha(description) {
|
|
1612
|
+
if (!description)
|
|
1613
|
+
return null;
|
|
1614
|
+
const m = description.match(REVIEWED_SHA_RE);
|
|
1615
|
+
return m ? m[1] : null;
|
|
1616
|
+
}
|
|
1617
|
+
function upsertReviewedSha(description, sha) {
|
|
1618
|
+
const line = `Reviewed-SHA: ${sha}`;
|
|
1619
|
+
if (REVIEWED_SHA_RE.test(description)) {
|
|
1620
|
+
return description.replace(REVIEWED_SHA_RE, line);
|
|
1621
|
+
}
|
|
1622
|
+
const sep = description ? `
|
|
1623
|
+
` : "";
|
|
1624
|
+
return `${description}${sep}${line}`;
|
|
1625
|
+
}
|
|
1626
|
+
function deriveCiStatus(rollup) {
|
|
1627
|
+
if (!Array.isArray(rollup) || rollup.length === 0)
|
|
1628
|
+
return "unknown";
|
|
1629
|
+
let anyPending = false;
|
|
1630
|
+
for (const check of rollup) {
|
|
1631
|
+
if (typeof check !== "object" || check === null)
|
|
1632
|
+
continue;
|
|
1633
|
+
const c = check;
|
|
1634
|
+
if (typeof c.status === "string") {
|
|
1635
|
+
if (c.status.toUpperCase() !== "COMPLETED") {
|
|
1636
|
+
anyPending = true;
|
|
1637
|
+
continue;
|
|
1638
|
+
}
|
|
1639
|
+
const conclusion = typeof c.conclusion === "string" ? c.conclusion.toUpperCase() : "";
|
|
1640
|
+
if (["SUCCESS", "NEUTRAL", "SKIPPED"].includes(conclusion))
|
|
1641
|
+
continue;
|
|
1642
|
+
return "failure";
|
|
1643
|
+
}
|
|
1644
|
+
if (typeof c.state === "string") {
|
|
1645
|
+
const state = c.state.toUpperCase();
|
|
1646
|
+
if (state === "SUCCESS")
|
|
1647
|
+
continue;
|
|
1648
|
+
if (state === "PENDING") {
|
|
1649
|
+
anyPending = true;
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
return "failure";
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
return anyPending ? "pending" : "success";
|
|
1656
|
+
}
|
|
1657
|
+
async function getPrStatus(prUrl, cwd, provider) {
|
|
1658
|
+
if (provider !== "github" || !isValidPrUrl(prUrl)) {
|
|
1659
|
+
return { ciStatus: "unknown", headSha: null };
|
|
1660
|
+
}
|
|
1661
|
+
try {
|
|
1662
|
+
const { stdout } = await execFileAsync("gh", ["pr", "view", prUrl, "--json", "statusCheckRollup,headRefOid"], { cwd, encoding: "utf-8", timeout: 1e4 });
|
|
1663
|
+
const parsed = JSON.parse(stdout.trim());
|
|
1664
|
+
const headSha = typeof parsed.headRefOid === "string" ? parsed.headRefOid : null;
|
|
1665
|
+
return { ciStatus: deriveCiStatus(parsed.statusCheckRollup), headSha };
|
|
1666
|
+
} catch {
|
|
1667
|
+
return { ciStatus: "unknown", headSha: null };
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
async function mergePullRequest(prUrl, cwd, provider, strategy, deleteBranch) {
|
|
1671
|
+
if (provider !== "github") {
|
|
1672
|
+
throw new Error(`auto-merge unsupported for provider "${provider}"`);
|
|
1673
|
+
}
|
|
1674
|
+
const args = ["pr", "merge", prUrl, `--${strategy}`];
|
|
1675
|
+
if (deleteBranch)
|
|
1676
|
+
args.push("--delete-branch");
|
|
1677
|
+
await execFileAsync("gh", args, { cwd, encoding: "utf-8", timeout: 30000 });
|
|
1678
|
+
}
|
|
1679
|
+
function getHeadSha(cwd) {
|
|
1680
|
+
try {
|
|
1681
|
+
return execFileSync("git", ["rev-parse", "HEAD"], {
|
|
1682
|
+
cwd,
|
|
1683
|
+
encoding: "utf-8"
|
|
1684
|
+
}).trim();
|
|
1685
|
+
} catch {
|
|
1686
|
+
return null;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
async function checkPrMergeStatus(prUrl, cwd, provider) {
|
|
1690
|
+
if (!isValidPrUrl(prUrl))
|
|
1691
|
+
return "unknown";
|
|
1692
|
+
try {
|
|
1693
|
+
switch (provider) {
|
|
1694
|
+
case "github": {
|
|
1695
|
+
const { stdout } = await execFileAsync("gh", ["pr", "view", prUrl, "--json", "state", "--jq", ".state"], { cwd, encoding: "utf-8", timeout: 1e4 });
|
|
1696
|
+
switch (stdout.trim()) {
|
|
1697
|
+
case "MERGED":
|
|
1698
|
+
return "merged";
|
|
1699
|
+
case "OPEN":
|
|
1700
|
+
return "open";
|
|
1701
|
+
case "CLOSED":
|
|
1702
|
+
return "closed";
|
|
1703
|
+
default:
|
|
1704
|
+
return "unknown";
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
case "gitlab": {
|
|
1708
|
+
const mrMatch = prUrl.match(/merge_requests\/(\d+)/);
|
|
1709
|
+
if (!mrMatch)
|
|
1710
|
+
return "unknown";
|
|
1711
|
+
const { stdout } = await execFileAsync("glab", ["mr", "view", mrMatch[1], "--output", "json"], { cwd, encoding: "utf-8", timeout: 1e4 });
|
|
1712
|
+
let parsed;
|
|
1713
|
+
try {
|
|
1714
|
+
parsed = JSON.parse(stdout.trim());
|
|
1715
|
+
} catch {
|
|
1716
|
+
log.warn(TAG2, `Failed to parse glab JSON output for MR ${mrMatch[1]}`);
|
|
1717
|
+
return "unknown";
|
|
1718
|
+
}
|
|
1719
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
1720
|
+
return "unknown";
|
|
1721
|
+
const state = parsed.state;
|
|
1722
|
+
if (state === "merged")
|
|
1723
|
+
return "merged";
|
|
1724
|
+
if (state === "opened")
|
|
1725
|
+
return "open";
|
|
1726
|
+
if (state === "closed")
|
|
1727
|
+
return "closed";
|
|
1728
|
+
return "unknown";
|
|
1729
|
+
}
|
|
1730
|
+
default:
|
|
1731
|
+
return "unknown";
|
|
1626
1732
|
}
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
}
|
|
1630
|
-
if (predicatePassed) {
|
|
1631
|
-
findings.push({
|
|
1632
|
-
level: "info",
|
|
1633
|
-
message: `Gate "${spec.kind}" predicate satisfied (mode "${mode}").`
|
|
1634
|
-
});
|
|
1733
|
+
} catch {
|
|
1734
|
+
return "unknown";
|
|
1635
1735
|
}
|
|
1636
|
-
return { passed: predicatePassed, findings, structured: safeStructured };
|
|
1637
1736
|
}
|
|
1638
|
-
function
|
|
1639
|
-
if (
|
|
1737
|
+
function decidePrBranch(provider, rawJson) {
|
|
1738
|
+
if (provider !== "github") {
|
|
1640
1739
|
return {
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
level: "error",
|
|
1644
|
-
message: "Malformed condition: not an object."
|
|
1645
|
-
}
|
|
1740
|
+
kind: "skip",
|
|
1741
|
+
reason: `PR-link review not yet supported for provider "${provider}" (GitHub only)`
|
|
1646
1742
|
};
|
|
1647
1743
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
return {
|
|
1651
|
-
ok: false,
|
|
1652
|
-
finding: {
|
|
1653
|
-
level: "error",
|
|
1654
|
-
message: "Malformed condition: missing string `path`."
|
|
1655
|
-
}
|
|
1656
|
-
};
|
|
1744
|
+
if (rawJson === null) {
|
|
1745
|
+
return { kind: "skip", reason: "gh pr view failed" };
|
|
1657
1746
|
}
|
|
1658
|
-
|
|
1747
|
+
let parsed;
|
|
1748
|
+
try {
|
|
1749
|
+
parsed = JSON.parse(rawJson.trim());
|
|
1750
|
+
} catch {
|
|
1751
|
+
return { kind: "skip", reason: "unparseable gh pr view output" };
|
|
1752
|
+
}
|
|
1753
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
1754
|
+
return { kind: "skip", reason: "unparseable gh pr view output" };
|
|
1755
|
+
}
|
|
1756
|
+
if (parsed.isCrossRepository === true) {
|
|
1659
1757
|
return {
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
level: "error",
|
|
1663
|
-
message: `Unknown operator ${formatValue(cond.op)} at "${cond.path}"; failing closed.`,
|
|
1664
|
-
path: cond.path
|
|
1665
|
-
}
|
|
1758
|
+
kind: "skip",
|
|
1759
|
+
reason: "fork PR (cross-repo head branch not on origin)"
|
|
1666
1760
|
};
|
|
1667
1761
|
}
|
|
1668
|
-
const
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
let ok;
|
|
1672
|
-
switch (op) {
|
|
1673
|
-
case "exists":
|
|
1674
|
-
ok = actual !== undefined;
|
|
1675
|
-
break;
|
|
1676
|
-
case "eq":
|
|
1677
|
-
ok = strictEquals(actual, expected);
|
|
1678
|
-
break;
|
|
1679
|
-
case "neq":
|
|
1680
|
-
ok = !strictEquals(actual, expected);
|
|
1681
|
-
break;
|
|
1682
|
-
case "gte":
|
|
1683
|
-
case "gt":
|
|
1684
|
-
case "lte":
|
|
1685
|
-
case "lt":
|
|
1686
|
-
ok = numericCompare(op, actual, expected);
|
|
1687
|
-
break;
|
|
1688
|
-
case "contains":
|
|
1689
|
-
ok = containsCheck(actual, expected);
|
|
1690
|
-
break;
|
|
1691
|
-
default: {
|
|
1692
|
-
const _never = op;
|
|
1693
|
-
ok = false;
|
|
1694
|
-
}
|
|
1762
|
+
const branch = typeof parsed.headRefName === "string" ? parsed.headRefName : null;
|
|
1763
|
+
if (!branch) {
|
|
1764
|
+
return { kind: "skip", reason: "PR has no head branch name" };
|
|
1695
1765
|
}
|
|
1696
|
-
if (
|
|
1697
|
-
return {
|
|
1698
|
-
return {
|
|
1699
|
-
ok: false,
|
|
1700
|
-
finding: {
|
|
1701
|
-
level: "error",
|
|
1702
|
-
message: `Condition failed: ${cond.path} ${op} ${formatValue(expected)} (actual: ${formatValue(actual)}).`,
|
|
1703
|
-
path: cond.path
|
|
1704
|
-
}
|
|
1705
|
-
};
|
|
1706
|
-
}
|
|
1707
|
-
function isPlainObject(value) {
|
|
1708
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1709
|
-
}
|
|
1710
|
-
function resolvePath(root, path) {
|
|
1711
|
-
const segments = path.split(".");
|
|
1712
|
-
let current = root;
|
|
1713
|
-
for (const segment of segments) {
|
|
1714
|
-
if (current === null || current === undefined)
|
|
1715
|
-
return;
|
|
1716
|
-
if (Array.isArray(current)) {
|
|
1717
|
-
const index = Number(segment);
|
|
1718
|
-
if (!Number.isInteger(index) || index < 0 || index >= current.length) {
|
|
1719
|
-
return;
|
|
1720
|
-
}
|
|
1721
|
-
current = current[index];
|
|
1722
|
-
} else if (typeof current === "object") {
|
|
1723
|
-
if (!Object.hasOwn(current, segment)) {
|
|
1724
|
-
return;
|
|
1725
|
-
}
|
|
1726
|
-
current = current[segment];
|
|
1727
|
-
} else {
|
|
1728
|
-
return;
|
|
1729
|
-
}
|
|
1766
|
+
if (!SAFE_GIT_REF_PATTERN.test(branch)) {
|
|
1767
|
+
return { kind: "skip", reason: `unsafe git ref: ${branch}` };
|
|
1730
1768
|
}
|
|
1731
|
-
return
|
|
1732
|
-
}
|
|
1733
|
-
function strictEquals(a, b) {
|
|
1734
|
-
if (a === null || b === null)
|
|
1735
|
-
return a === b;
|
|
1736
|
-
const t = typeof a;
|
|
1737
|
-
if (t !== "string" && t !== "number" && t !== "boolean")
|
|
1738
|
-
return false;
|
|
1739
|
-
return a === b;
|
|
1769
|
+
return { kind: "branch", branch };
|
|
1740
1770
|
}
|
|
1741
|
-
function
|
|
1742
|
-
if (
|
|
1743
|
-
return
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
return actual > expected;
|
|
1751
|
-
case "lte":
|
|
1752
|
-
return actual <= expected;
|
|
1753
|
-
case "lt":
|
|
1754
|
-
return actual < expected;
|
|
1771
|
+
async function resolvePrHeadBranch(prUrl, cwd, provider) {
|
|
1772
|
+
if (provider !== "github")
|
|
1773
|
+
return decidePrBranch(provider, null);
|
|
1774
|
+
try {
|
|
1775
|
+
const { stdout } = await execFileAsync("gh", ["pr", "view", prUrl, "--json", "headRefName,isCrossRepository"], { cwd, encoding: "utf-8", timeout: 1e4 });
|
|
1776
|
+
return decidePrBranch("github", stdout);
|
|
1777
|
+
} catch (err) {
|
|
1778
|
+
log.warn(TAG2, `gh pr view failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1779
|
+
return decidePrBranch("github", null);
|
|
1755
1780
|
}
|
|
1756
1781
|
}
|
|
1757
|
-
function
|
|
1758
|
-
if (
|
|
1759
|
-
return
|
|
1760
|
-
|
|
1761
|
-
if (
|
|
1762
|
-
return
|
|
1782
|
+
function extractPrUrl(description) {
|
|
1783
|
+
if (!description)
|
|
1784
|
+
return null;
|
|
1785
|
+
const match = description.match(PR_URL_RE);
|
|
1786
|
+
if (!match)
|
|
1787
|
+
return null;
|
|
1788
|
+
try {
|
|
1789
|
+
return new URL(match[1]).href;
|
|
1790
|
+
} catch {
|
|
1791
|
+
return null;
|
|
1763
1792
|
}
|
|
1764
|
-
return false;
|
|
1765
1793
|
}
|
|
1766
|
-
function
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
}
|
|
1794
|
+
function resolvePrUrl(description, branchName, cwd, provider) {
|
|
1795
|
+
const fromDesc = extractPrUrl(description);
|
|
1796
|
+
if (fromDesc)
|
|
1797
|
+
return fromDesc;
|
|
1798
|
+
if (!branchName)
|
|
1799
|
+
return null;
|
|
1800
|
+
return findExistingPr(branchName, cwd, provider) || null;
|
|
1801
|
+
}
|
|
1802
|
+
function remoteBranchExists(branchName, cwd) {
|
|
1776
1803
|
try {
|
|
1777
|
-
|
|
1804
|
+
execFileSync("git", ["ls-remote", "--exit-code", "origin", `refs/heads/${branchName}`], { cwd, stdio: "pipe" });
|
|
1805
|
+
return true;
|
|
1778
1806
|
} catch {
|
|
1779
|
-
return
|
|
1807
|
+
return false;
|
|
1780
1808
|
}
|
|
1781
1809
|
}
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
"
|
|
1796
|
-
"
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
"
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
// ../harmony-shared/dist/gateEvidence.js
|
|
1806
|
-
function toStageGateEvidenceInsert(context, evidence) {
|
|
1807
|
-
return {
|
|
1808
|
-
card_id: context.cardId,
|
|
1809
|
-
workspace_id: context.workspaceId,
|
|
1810
|
-
stage_id: context.stageId,
|
|
1811
|
-
gate_kind: context.gate.kind,
|
|
1812
|
-
result: evidence.result,
|
|
1813
|
-
structured: evidence.structured
|
|
1814
|
-
};
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
// ../harmony-shared/dist/logger.js
|
|
1818
|
-
var init_logger = () => {};
|
|
1819
|
-
// ../harmony-shared/dist/playbookCatalog.js
|
|
1820
|
-
var init_playbookCatalog = () => {};
|
|
1821
|
-
|
|
1822
|
-
// ../harmony-shared/dist/playbookStage.js
|
|
1823
|
-
function normalizeLoopDef(raw) {
|
|
1824
|
-
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
1825
|
-
return null;
|
|
1826
|
-
const obj = raw;
|
|
1827
|
-
if (obj.mode !== "converge" && obj.mode !== "fanout")
|
|
1828
|
-
return null;
|
|
1829
|
-
const mode = obj.mode;
|
|
1830
|
-
const rawMax = obj.max_iterations;
|
|
1831
|
-
const maxInt = typeof rawMax === "number" && Number.isFinite(rawMax) && rawMax >= 1 ? Math.floor(rawMax) : DEFAULT_LOOP_MAX_ITERATIONS;
|
|
1832
|
-
const exitGate = obj.exit_gate && typeof obj.exit_gate === "object" && !Array.isArray(obj.exit_gate) ? obj.exit_gate : null;
|
|
1833
|
-
const def = { mode, max_iterations: maxInt };
|
|
1834
|
-
if (exitGate)
|
|
1835
|
-
def.exit_gate = exitGate;
|
|
1836
|
-
if (obj.item_source && typeof obj.item_source === "object" && !Array.isArray(obj.item_source)) {
|
|
1837
|
-
def.item_source = obj.item_source;
|
|
1810
|
+
function pushBranch(branchName, cwd) {
|
|
1811
|
+
if (remoteBranchExists(branchName, cwd)) {
|
|
1812
|
+
log.info(TAG2, `Remote branch ${branchName} exists (rework), force-pushing`);
|
|
1813
|
+
let expectedSha = null;
|
|
1814
|
+
try {
|
|
1815
|
+
execFileSync("git", ["fetch", "origin", branchName], {
|
|
1816
|
+
cwd,
|
|
1817
|
+
stdio: "pipe"
|
|
1818
|
+
});
|
|
1819
|
+
expectedSha = execFileSync("git", ["rev-parse", `refs/remotes/origin/${branchName}`], { cwd, encoding: "utf-8" }).trim();
|
|
1820
|
+
} catch (err) {
|
|
1821
|
+
log.warn(TAG2, `could not resolve remote tip for ${branchName}, falling back to weak lease: ${err instanceof Error ? err.message : err}`);
|
|
1822
|
+
}
|
|
1823
|
+
const lease = expectedSha ? `--force-with-lease=refs/heads/${branchName}:${expectedSha}` : "--force-with-lease";
|
|
1824
|
+
execFileSync("git", ["push", lease, "-u", "origin", branchName], {
|
|
1825
|
+
cwd,
|
|
1826
|
+
stdio: "pipe"
|
|
1827
|
+
});
|
|
1828
|
+
} else {
|
|
1829
|
+
execFileSync("git", ["push", "-u", "origin", branchName], {
|
|
1830
|
+
cwd,
|
|
1831
|
+
stdio: "pipe"
|
|
1832
|
+
});
|
|
1838
1833
|
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1834
|
+
}
|
|
1835
|
+
function renameRemoteBranch(oldRef, newRef, cwd) {
|
|
1836
|
+
if (oldRef === newRef)
|
|
1837
|
+
return;
|
|
1838
|
+
let sha;
|
|
1839
|
+
try {
|
|
1840
|
+
sha = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
1841
|
+
cwd,
|
|
1842
|
+
encoding: "utf-8"
|
|
1843
|
+
}).trim();
|
|
1844
|
+
} catch (err) {
|
|
1845
|
+
throw new Error(`renameRemoteBranch: could not resolve HEAD: ${err instanceof Error ? err.message : err}`);
|
|
1841
1846
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1847
|
+
log.info(TAG2, `Renaming remote ${oldRef} → ${newRef}`);
|
|
1848
|
+
execFileSync("git", ["push", "origin", `${sha}:refs/heads/${newRef}`, "--force-with-lease"], { cwd, stdio: "pipe" });
|
|
1849
|
+
try {
|
|
1850
|
+
execFileSync("git", ["push", "origin", `:refs/heads/${oldRef}`], {
|
|
1851
|
+
cwd,
|
|
1852
|
+
stdio: "pipe"
|
|
1853
|
+
});
|
|
1854
|
+
} catch (err) {
|
|
1855
|
+
log.warn(TAG2, `renameRemoteBranch: could not delete old ref ${oldRef}: ${err instanceof Error ? err.message : err}`);
|
|
1844
1856
|
}
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
return loop !== null && loop.mode === "converge";
|
|
1852
|
-
}
|
|
1853
|
-
function resolveLoopExitGate(stage, loop) {
|
|
1854
|
-
return loop.exit_gate ?? stage.gate ?? null;
|
|
1857
|
+
try {
|
|
1858
|
+
execFileSync("git", ["branch", "-m", oldRef, newRef], {
|
|
1859
|
+
cwd,
|
|
1860
|
+
stdio: "pipe"
|
|
1861
|
+
});
|
|
1862
|
+
} catch {}
|
|
1855
1863
|
}
|
|
1856
|
-
function
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1864
|
+
function getBranchWebUrl(branchName, cwd) {
|
|
1865
|
+
try {
|
|
1866
|
+
const remoteUrl = execFileSync("git", ["remote", "get-url", "origin"], {
|
|
1867
|
+
cwd,
|
|
1868
|
+
encoding: "utf-8"
|
|
1869
|
+
}).trim();
|
|
1870
|
+
const encoded = branchName.split("/").map(encodeURIComponent).join("/");
|
|
1871
|
+
if (/github\.com[:/]([^/]+)\/([^/.]+)/.test(remoteUrl)) {
|
|
1872
|
+
const m = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
|
1873
|
+
if (m)
|
|
1874
|
+
return `https://github.com/${m[1]}/${m[2]}/tree/${encoded}`;
|
|
1875
|
+
}
|
|
1876
|
+
if (/gitlab\.com[:/]([^/]+)\/([^/.]+)/.test(remoteUrl)) {
|
|
1877
|
+
const m = remoteUrl.match(/gitlab\.com[:/](.+?)(?:\.git)?$/);
|
|
1878
|
+
if (m)
|
|
1879
|
+
return `https://gitlab.com/${m[1]}/-/tree/${encoded}`;
|
|
1880
|
+
}
|
|
1881
|
+
if (/bitbucket\.org[:/]([^/]+)\/([^/.]+)/.test(remoteUrl)) {
|
|
1882
|
+
const m = remoteUrl.match(/bitbucket\.org[:/](.+?)(?:\.git)?$/);
|
|
1883
|
+
if (m)
|
|
1884
|
+
return `https://bitbucket.org/${m[1]}/branch/${encoded}`;
|
|
1885
|
+
}
|
|
1886
|
+
return null;
|
|
1887
|
+
} catch {
|
|
1888
|
+
return null;
|
|
1861
1889
|
}
|
|
1862
|
-
if (gatePassed)
|
|
1863
|
-
return "exit";
|
|
1864
|
-
if (completedIterations >= max)
|
|
1865
|
-
return "exhausted";
|
|
1866
|
-
return "iterate";
|
|
1867
|
-
}
|
|
1868
|
-
function readStageDefs(def) {
|
|
1869
|
-
if (def.steps_version !== 2)
|
|
1870
|
-
return [];
|
|
1871
|
-
return Array.isArray(def.steps) ? def.steps : [];
|
|
1872
|
-
}
|
|
1873
|
-
function resolveStageDef(def, currentStage) {
|
|
1874
|
-
if (def.steps_version !== 2)
|
|
1875
|
-
return { kind: "not_stage_model" };
|
|
1876
|
-
const stages = readStageDefs(def);
|
|
1877
|
-
const index = stages.findIndex((s) => s?.id === currentStage);
|
|
1878
|
-
if (index === -1)
|
|
1879
|
-
return { kind: "stage_not_found" };
|
|
1880
|
-
return { kind: "found", stage: stages[index], index };
|
|
1881
|
-
}
|
|
1882
|
-
function isAgentRunnableOwner(owner) {
|
|
1883
|
-
return owner === "agent" || owner === "either";
|
|
1884
1890
|
}
|
|
1885
|
-
function
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1891
|
+
function buildPrBody(card, commitLog) {
|
|
1892
|
+
return [
|
|
1893
|
+
"## Summary",
|
|
1894
|
+
"",
|
|
1895
|
+
`Automated PR for card **#${card.short_id} — ${card.title}**.`,
|
|
1896
|
+
"",
|
|
1897
|
+
"## Commits",
|
|
1898
|
+
"",
|
|
1899
|
+
"```",
|
|
1900
|
+
commitLog,
|
|
1901
|
+
"```",
|
|
1902
|
+
"",
|
|
1903
|
+
"## Card",
|
|
1904
|
+
"",
|
|
1905
|
+
card.description?.slice(0, 500) || "No description.",
|
|
1906
|
+
"",
|
|
1907
|
+
"---",
|
|
1908
|
+
"*Created by Harmony Agent Daemon*"
|
|
1909
|
+
].join(`
|
|
1910
|
+
`);
|
|
1893
1911
|
}
|
|
1894
|
-
function
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1912
|
+
function createPullRequest(card, branchName, worktreePath, config, provider) {
|
|
1913
|
+
let commitLog = "";
|
|
1914
|
+
try {
|
|
1915
|
+
commitLog = execFileSync("git", ["log", "--oneline", `origin/${config.worktree.baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
1916
|
+
} catch {
|
|
1917
|
+
commitLog = "(unable to retrieve commit log)";
|
|
1918
|
+
}
|
|
1919
|
+
const title = `#${card.short_id} ${card.title}`;
|
|
1920
|
+
const body = buildPrBody(card, commitLog);
|
|
1921
|
+
const base = config.worktree.baseBranch;
|
|
1922
|
+
const existingUrl = findExistingPr(branchName, worktreePath, provider);
|
|
1923
|
+
if (existingUrl) {
|
|
1924
|
+
log.info(TAG2, `PR already exists for ${branchName}, updating body...`);
|
|
1925
|
+
updateExistingPr(branchName, body, worktreePath, provider);
|
|
1926
|
+
return existingUrl;
|
|
1927
|
+
}
|
|
1928
|
+
try {
|
|
1929
|
+
let result;
|
|
1930
|
+
switch (provider) {
|
|
1931
|
+
case "github":
|
|
1932
|
+
result = execFileSync("gh", ["pr", "create", "--title", title, "--body", body, "--base", base], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
1933
|
+
break;
|
|
1934
|
+
case "azure": {
|
|
1935
|
+
const azOutput = execFileSync("az", [
|
|
1936
|
+
"repos",
|
|
1937
|
+
"pr",
|
|
1938
|
+
"create",
|
|
1939
|
+
"--title",
|
|
1940
|
+
title,
|
|
1941
|
+
"--description",
|
|
1942
|
+
body,
|
|
1943
|
+
"--source-branch",
|
|
1944
|
+
branchName,
|
|
1945
|
+
"--target-branch",
|
|
1946
|
+
base,
|
|
1947
|
+
"--auto-complete",
|
|
1948
|
+
"false"
|
|
1949
|
+
], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
1950
|
+
try {
|
|
1951
|
+
const parsed = JSON.parse(azOutput);
|
|
1952
|
+
result = parsed.remoteUrl ?? parsed.url ?? azOutput;
|
|
1953
|
+
} catch {
|
|
1954
|
+
result = azOutput;
|
|
1955
|
+
}
|
|
1956
|
+
break;
|
|
1957
|
+
}
|
|
1958
|
+
case "gitlab":
|
|
1959
|
+
result = execFileSync("glab", [
|
|
1960
|
+
"mr",
|
|
1961
|
+
"create",
|
|
1962
|
+
"--title",
|
|
1963
|
+
title,
|
|
1964
|
+
"--description",
|
|
1965
|
+
body,
|
|
1966
|
+
"--source-branch",
|
|
1967
|
+
branchName,
|
|
1968
|
+
"--target-branch",
|
|
1969
|
+
base,
|
|
1970
|
+
"--no-editor"
|
|
1971
|
+
], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
1972
|
+
break;
|
|
1973
|
+
default:
|
|
1974
|
+
log.warn(TAG2, `No PR CLI for provider "${provider}" — branch pushed but no PR created`);
|
|
1975
|
+
return null;
|
|
1904
1976
|
}
|
|
1905
|
-
|
|
1977
|
+
log.info(TAG2, `PR created: ${result}`);
|
|
1978
|
+
return result;
|
|
1979
|
+
} catch (err) {
|
|
1980
|
+
log.error(TAG2, `Failed to create PR: ${err instanceof Error ? err.message : err}`);
|
|
1981
|
+
return null;
|
|
1906
1982
|
}
|
|
1907
|
-
return null;
|
|
1908
1983
|
}
|
|
1909
|
-
function
|
|
1910
|
-
|
|
1984
|
+
function findExistingPr(branchName, worktreePath, provider) {
|
|
1985
|
+
try {
|
|
1986
|
+
switch (provider) {
|
|
1987
|
+
case "github":
|
|
1988
|
+
return execFileSync("gh", ["pr", "view", branchName, "--json", "url", "--jq", ".url"], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
1989
|
+
case "gitlab": {
|
|
1990
|
+
const json = execFileSync("glab", ["mr", "view", branchName, "--output", "json"], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
1991
|
+
const parsed = JSON.parse(json);
|
|
1992
|
+
return parsed.web_url || null;
|
|
1993
|
+
}
|
|
1994
|
+
default:
|
|
1995
|
+
return null;
|
|
1996
|
+
}
|
|
1997
|
+
} catch {
|
|
1998
|
+
return null;
|
|
1999
|
+
}
|
|
1911
2000
|
}
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
2001
|
+
function updateExistingPr(branchName, body, worktreePath, provider) {
|
|
2002
|
+
try {
|
|
2003
|
+
switch (provider) {
|
|
2004
|
+
case "github":
|
|
2005
|
+
execFileSync("gh", ["pr", "edit", branchName, "--body", body], {
|
|
2006
|
+
cwd: worktreePath,
|
|
2007
|
+
stdio: "pipe"
|
|
2008
|
+
});
|
|
2009
|
+
break;
|
|
2010
|
+
case "gitlab":
|
|
2011
|
+
execFileSync("glab", ["mr", "update", branchName, "--description", body], { cwd: worktreePath, stdio: "pipe" });
|
|
2012
|
+
break;
|
|
2013
|
+
}
|
|
2014
|
+
log.info(TAG2, `Updated existing PR body for ${branchName}`);
|
|
2015
|
+
} catch (err) {
|
|
2016
|
+
log.warn(TAG2, `Failed to update PR body: ${err instanceof Error ? err.message : err}`);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
var execFileAsync, TAG2 = "git-pr", VALID_PR_URL_RE, PR_URL_RE, REVIEWED_SHA_RE;
|
|
2020
|
+
var init_git_pr = __esm(() => {
|
|
2021
|
+
init_dist();
|
|
2022
|
+
init_log();
|
|
2023
|
+
execFileAsync = promisify(execFile);
|
|
2024
|
+
VALID_PR_URL_RE = /^https:\/\/(github\.com|gitlab\.com|dev\.azure\.com|bitbucket\.org)\//;
|
|
2025
|
+
PR_URL_RE = /PR:\s*(https?:\/\/[^\s)]+)/;
|
|
2026
|
+
REVIEWED_SHA_RE = /^Reviewed-SHA:\s*([0-9a-f]{7,40})\s*$/im;
|
|
1928
2027
|
});
|
|
1929
2028
|
|
|
1930
|
-
//
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
var REVIEW_SYSTEM_PROMPT = `You are a senior code reviewer. Follow this two-pass methodology strictly.
|
|
1935
|
-
Report findings; do NOT fix them. This is a read-only review.
|
|
1936
|
-
|
|
1937
|
-
Review the diff through five lenses on every pass: functionality, security,
|
|
1938
|
-
performance, code quality, and best practices. For every finding, set
|
|
1939
|
-
\`relatedToDiff\`: true when the change under review introduced or exposed it,
|
|
1940
|
-
false when it is a pre-existing issue you happened to notice. Only diff-caused
|
|
1941
|
-
findings gate the verdict — pre-existing ones are reported for context and never
|
|
1942
|
-
block.
|
|
1943
|
-
|
|
1944
|
-
## Two-Pass Review
|
|
1945
|
-
|
|
1946
|
-
### Pass 1 — CRITICAL (highest severity)
|
|
1947
|
-
|
|
1948
|
-
**SQL & Data Safety**
|
|
1949
|
-
- String interpolation in SQL — use parameterized queries / prepared statements
|
|
1950
|
-
- TOCTOU races: check-then-set patterns that should be atomic WHERE + UPDATE
|
|
1951
|
-
|
|
1952
|
-
**Race Conditions & Concurrency**
|
|
1953
|
-
- Read-check-write without uniqueness constraint or duplicate key handling
|
|
1954
|
-
- Status transitions without atomic WHERE old_status UPDATE SET new_status
|
|
1955
|
-
- Unsafe HTML rendering (dangerouslySetInnerHTML, v-html) on user-controlled data (XSS)
|
|
1956
|
-
|
|
1957
|
-
**Security & Access Control**
|
|
1958
|
-
- Hardcoded secrets, API keys, or credentials committed to source
|
|
1959
|
-
- New endpoints, mutations, or service-role/RLS-exempt queries missing an auth or ownership check
|
|
1960
|
-
- Over-broad CORS, missing input validation on a trust boundary, injection beyond SQL (command, path, template)
|
|
1961
|
-
|
|
1962
|
-
**LLM Output Trust Boundary**
|
|
1963
|
-
- LLM-generated values written to DB without format validation (EMAIL_REGEXP, URI.parse, .trim())
|
|
1964
|
-
- Structured tool output accepted without type/shape checks before database writes
|
|
1965
|
-
|
|
1966
|
-
**Enum & Value Completeness**
|
|
1967
|
-
- When the diff introduces a new enum/status/type value, trace it through every consumer
|
|
1968
|
-
- Check allowlists, filter arrays, and case/if-elsif chains for the new value
|
|
1969
|
-
- Use Grep to find all references to sibling values and Read each match — look OUTSIDE the diff
|
|
1970
|
-
|
|
1971
|
-
### Pass 2 — INFORMATIONAL (lower severity)
|
|
1972
|
-
|
|
1973
|
-
**Functionality & Edge Cases**
|
|
1974
|
-
- Logic errors, off-by-one, unhandled null/undefined, wrong API or library usage
|
|
1975
|
-
- Conditional side effects: code paths that branch but forget a side effect on one branch (e.g., promoting without attaching URL)
|
|
1976
|
-
|
|
1977
|
-
**Performance**
|
|
1978
|
-
- O(n²) algorithms and O(n*m) lookups (Array.find in a loop instead of a Map/index)
|
|
1979
|
-
- N+1 queries, unbounded fetches missing pagination, repeated work that should be cached/memoized
|
|
1980
|
-
- Unnecessary React re-renders (unstable props/deps, inline object/array literals); leaked subscriptions, timers, or listeners
|
|
1981
|
-
- Inline styles re-parsed every render
|
|
1982
|
-
|
|
1983
|
-
**Code Quality**
|
|
1984
|
-
- Dead code: variables assigned but never read, unreachable branches
|
|
1985
|
-
- Duplication that should be extracted, over-long functions, unclear naming
|
|
1986
|
-
- \`any\` / unchecked casts that defeat the type system
|
|
1987
|
-
- Comments/docstrings describing old behavior after code changed
|
|
1988
|
-
|
|
1989
|
-
**Best Practices & Conventions**
|
|
1990
|
-
- Deviations from established project conventions and framework idioms / anti-patterns
|
|
1991
|
-
- React hook dependency arrays that are wrong, missing, or over-broad
|
|
1992
|
-
- Accessibility gaps on new UI: missing labels, roles, alt text, or keyboard paths
|
|
1993
|
-
|
|
1994
|
-
**Test Gaps**
|
|
1995
|
-
- Missing negative-path tests for new error handling
|
|
1996
|
-
- Security enforcement features without integration tests
|
|
1997
|
-
|
|
1998
|
-
**Completeness Gaps**
|
|
1999
|
-
- Partial enum handling, incomplete error paths, missing edge cases that are straightforward to add
|
|
2000
|
-
|
|
2001
|
-
## Severity Classification
|
|
2002
|
-
|
|
2003
|
-
- **critical**: SQL safety, race conditions, XSS, secrets/auth/injection holes, LLM trust boundary violations, enum completeness gaps causing runtime errors
|
|
2004
|
-
- **major**: Missing requirements, broken functionality, significant completeness gaps, conditional side effects, performance regressions on a hot path
|
|
2005
|
-
- **minor**: Dead code, stale comments, test gaps, naming/duplication, minor view issues, cosmetic completeness gaps
|
|
2006
|
-
|
|
2007
|
-
## Suppressions — DO NOT flag these
|
|
2008
|
-
|
|
2009
|
-
- Redundancy that aids readability (e.g., present? redundant with length > 20)
|
|
2010
|
-
- "Add a comment explaining why this threshold was chosen" — thresholds change, comments rot
|
|
2011
|
-
- Consistency-only changes (wrapping a value to match how another constant is guarded)
|
|
2012
|
-
- Regex edge cases when input is constrained and the edge case never occurs in practice
|
|
2013
|
-
- Eval threshold changes — these are tuned empirically
|
|
2014
|
-
- Harmless no-ops (e.g., .reject on an element never in the array)
|
|
2015
|
-
- Pre-existing issues unrelated to the diff, beyond a single noted finding (set relatedToDiff:false; never block on them)
|
|
2016
|
-
- ANYTHING already addressed in the diff you are reviewing — read the FULL diff before flagging`, REVIEW_ACCEPTANCE_CHECKS = `## Acceptance Checks
|
|
2017
|
-
|
|
2018
|
-
Before judging code quality, verify the change actually satisfies the card.
|
|
2019
|
-
Derive one acceptance check per concrete requirement in the card description and
|
|
2020
|
-
one per subtask (the stated acceptance criteria). For each, assign a status from
|
|
2021
|
-
hard evidence — cite the file:line you read or the dev-server behaviour you
|
|
2022
|
-
observed that proves it:
|
|
2023
|
-
|
|
2024
|
-
- **pass** — implemented and verified by code you read or behaviour you observed
|
|
2025
|
-
- **partial** — started but incomplete (a missing branch, an edge case, or one of several bundled requirements)
|
|
2026
|
-
- **fail** — required but absent, or implemented incorrectly
|
|
2027
|
-
- **unverifiable** — cannot be confirmed from the diff or a running app (state why)
|
|
2028
|
-
|
|
2029
|
-
Do NOT mark a check "pass" on the implementing agent's say-so or a subtask's
|
|
2030
|
-
checkbox alone — only on evidence you found yourself. Any \`fail\` or \`partial\`
|
|
2031
|
-
check is an unaddressed requirement and forces a rejected verdict.`, QA_VISUAL_CHECKLIST = `## Visual QA Checklist
|
|
2032
|
-
|
|
2033
|
-
For each page affected by the changes:
|
|
2034
|
-
|
|
2035
|
-
1. **Visual scan** — Screenshot the page. Check for layout breaks, broken images, alignment issues, z-index problems.
|
|
2036
|
-
2. **Interactive elements** — Click every button, link, and control. Does each do what it says?
|
|
2037
|
-
3. **Forms** — Fill and submit. Test empty submission, invalid data, edge cases.
|
|
2038
|
-
4. **Navigation** — Check all paths in/out. Breadcrumbs, back button, deep links.
|
|
2039
|
-
5. **States** — Check empty state, loading state, error state, overflow state.
|
|
2040
|
-
6. **Console** — Check for JS exceptions, failed network requests (4xx/5xx), CORS errors after interactions.
|
|
2041
|
-
7. **Responsiveness** — If the change is visual, check mobile viewport (375px).
|
|
2029
|
+
// src/http-server.ts
|
|
2030
|
+
import {
|
|
2031
|
+
createServer
|
|
2032
|
+
} from "node:http";
|
|
2042
2033
|
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2034
|
+
class HttpServer {
|
|
2035
|
+
opts;
|
|
2036
|
+
server = null;
|
|
2037
|
+
boundPort = null;
|
|
2038
|
+
constructor(opts) {
|
|
2039
|
+
this.opts = opts;
|
|
2040
|
+
}
|
|
2041
|
+
get port() {
|
|
2042
|
+
return this.boundPort;
|
|
2043
|
+
}
|
|
2044
|
+
async start() {
|
|
2045
|
+
this.server = createServer((req, res) => {
|
|
2046
|
+
this.route(req, res).catch((err) => {
|
|
2047
|
+
log.error(TAG3, `unhandled: ${err instanceof Error ? err.message : err}`);
|
|
2048
|
+
if (!res.headersSent) {
|
|
2049
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
2050
|
+
res.end(JSON.stringify({ error: "internal_error" }));
|
|
2051
|
+
}
|
|
2052
|
+
});
|
|
2053
|
+
});
|
|
2054
|
+
const attempts = Math.max(1, this.opts.maxPortAttempts ?? 10);
|
|
2055
|
+
const startPort = this.opts.port;
|
|
2056
|
+
for (let i = 0;i < attempts; i++) {
|
|
2057
|
+
const port = startPort + i;
|
|
2058
|
+
try {
|
|
2059
|
+
await this.listenOnce(port);
|
|
2060
|
+
this.boundPort = port;
|
|
2061
|
+
if (port !== startPort) {
|
|
2062
|
+
log.info(TAG3, `port ${startPort} busy — bound to ${port} instead`);
|
|
2063
|
+
}
|
|
2064
|
+
return port;
|
|
2065
|
+
} catch (err) {
|
|
2066
|
+
const lastAttempt = i === attempts - 1;
|
|
2067
|
+
if (isAddrInUse(err) && !lastAttempt) {
|
|
2068
|
+
log.debug(TAG3, `port ${port} in use, trying ${port + 1}`);
|
|
2069
|
+
continue;
|
|
2070
|
+
}
|
|
2071
|
+
throw err;
|
|
2072
|
+
}
|
|
2059
2073
|
}
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2074
|
+
throw new Error("HTTP server failed to bind");
|
|
2075
|
+
}
|
|
2076
|
+
listenOnce(port) {
|
|
2077
|
+
return new Promise((resolve, reject) => {
|
|
2078
|
+
const server = this.server;
|
|
2079
|
+
if (!server) {
|
|
2080
|
+
reject(new Error("server not created"));
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
const onError = (err) => {
|
|
2084
|
+
server.removeListener("listening", onListening);
|
|
2085
|
+
reject(err);
|
|
2086
|
+
};
|
|
2087
|
+
const onListening = () => {
|
|
2088
|
+
server.removeListener("error", onError);
|
|
2089
|
+
resolve();
|
|
2090
|
+
};
|
|
2091
|
+
server.once("error", onError);
|
|
2092
|
+
server.once("listening", onListening);
|
|
2093
|
+
server.listen(port, this.opts.bindAddr);
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
async stop() {
|
|
2097
|
+
if (!this.server)
|
|
2098
|
+
return;
|
|
2099
|
+
await new Promise((resolve) => {
|
|
2100
|
+
this.server?.close(() => resolve());
|
|
2101
|
+
});
|
|
2102
|
+
this.server = null;
|
|
2103
|
+
this.boundPort = null;
|
|
2104
|
+
}
|
|
2105
|
+
async route(req, res) {
|
|
2106
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
2107
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
2108
|
+
const path = url.pathname;
|
|
2109
|
+
if (method === "GET" && path === "/health") {
|
|
2110
|
+
return this.respondHealth(res);
|
|
2111
|
+
}
|
|
2112
|
+
if (method === "GET" && path === "/status") {
|
|
2113
|
+
return this.respondStatus(res);
|
|
2114
|
+
}
|
|
2115
|
+
if (method === "POST") {
|
|
2116
|
+
const cmd = parseCommand(path);
|
|
2117
|
+
if (cmd) {
|
|
2118
|
+
return this.respondCommand(res, cmd.command, cmd.cardId);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
res.writeHead(404, { "content-type": "application/json" });
|
|
2122
|
+
res.end(JSON.stringify({ error: "not_found", path }));
|
|
2123
|
+
}
|
|
2124
|
+
respondHealth(res) {
|
|
2125
|
+
const health = this.opts.getHealth();
|
|
2126
|
+
res.writeHead(health.healthy ? 200 : 503, {
|
|
2127
|
+
"content-type": "application/json"
|
|
2128
|
+
});
|
|
2129
|
+
res.end(JSON.stringify(health));
|
|
2130
|
+
}
|
|
2131
|
+
respondStatus(res) {
|
|
2132
|
+
const snapshot = this.opts.getStatus();
|
|
2133
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
2134
|
+
res.end(JSON.stringify(snapshot));
|
|
2135
|
+
}
|
|
2136
|
+
async respondCommand(res, command, cardId) {
|
|
2137
|
+
try {
|
|
2138
|
+
await this.opts.handleCommand(command, cardId);
|
|
2139
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
2140
|
+
res.end(JSON.stringify({ ok: true, command, cardId }));
|
|
2141
|
+
} catch (err) {
|
|
2142
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
2143
|
+
res.end(JSON.stringify({
|
|
2144
|
+
error: "command_failed",
|
|
2145
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
2146
|
+
}));
|
|
2069
2147
|
}
|
|
2070
|
-
|
|
2071
|
-
}`, REVIEW_DECISION_RULES = `Counting only findings with \`relatedToDiff: true\`:
|
|
2072
|
-
- **rejected**: Any acceptance check that is \`fail\` or \`partial\`, any \`critical\` finding, unaddressed requirements, or 2+ \`major\` findings.
|
|
2073
|
-
- **approved**: Every acceptance check \`pass\` (or \`unverifiable\` with a stated reason), no critical findings, at most 1 major finding; minor findings OK.`;
|
|
2074
|
-
// ../harmony-shared/dist/stageHandoff.js
|
|
2075
|
-
function buildHandoffCommentBody(input) {
|
|
2076
|
-
const handoff = {
|
|
2077
|
-
version: STAGE_HANDOFF_VERSION,
|
|
2078
|
-
stageId: input.stageId,
|
|
2079
|
-
stageName: input.stageName,
|
|
2080
|
-
artifactType: input.artifactType,
|
|
2081
|
-
produced: input.produced,
|
|
2082
|
-
decisions: input.decisions,
|
|
2083
|
-
nextStageNeeds: input.nextStageNeeds,
|
|
2084
|
-
producedAt: input.producedAt ?? new Date().toISOString()
|
|
2085
|
-
};
|
|
2086
|
-
const decisionLines = handoff.decisions.length > 0 ? handoff.decisions.map((d) => `- ${d}`).join(`
|
|
2087
|
-
`) : "_None._";
|
|
2088
|
-
const prose = [
|
|
2089
|
-
`**Stage handoff — ${handoff.stageName}**`,
|
|
2090
|
-
"",
|
|
2091
|
-
`**Produced:** ${handoff.produced}`,
|
|
2092
|
-
"",
|
|
2093
|
-
"**Decisions (settled — do not re-litigate):**",
|
|
2094
|
-
decisionLines,
|
|
2095
|
-
"",
|
|
2096
|
-
`**What the next stage needs:** ${handoff.nextStageNeeds}`
|
|
2097
|
-
].join(`
|
|
2098
|
-
`);
|
|
2099
|
-
const payload = [
|
|
2100
|
-
"```json",
|
|
2101
|
-
`// ${HANDOFF_MARKER}`,
|
|
2102
|
-
JSON.stringify(handoff, null, 2),
|
|
2103
|
-
"```"
|
|
2104
|
-
].join(`
|
|
2105
|
-
`);
|
|
2106
|
-
return `${prose}
|
|
2107
|
-
|
|
2108
|
-
${payload}`;
|
|
2148
|
+
}
|
|
2109
2149
|
}
|
|
2110
|
-
function
|
|
2111
|
-
|
|
2112
|
-
return false;
|
|
2113
|
-
const v = value;
|
|
2114
|
-
return typeof v.stageId === "string" && typeof v.stageName === "string" && typeof v.produced === "string" && typeof v.nextStageNeeds === "string" && typeof v.producedAt === "string" && Array.isArray(v.decisions) && v.decisions.every((d) => typeof d === "string") && (v.artifactType === null || typeof v.artifactType === "string") && v.version === STAGE_HANDOFF_VERSION;
|
|
2150
|
+
function isAddrInUse(err) {
|
|
2151
|
+
return typeof err === "object" && err !== null && err.code === "EADDRINUSE";
|
|
2115
2152
|
}
|
|
2116
|
-
function
|
|
2117
|
-
const match =
|
|
2153
|
+
function parseCommand(path) {
|
|
2154
|
+
const match = path.match(/^\/(pause|resume|stop)\/([^/]+)$/);
|
|
2118
2155
|
if (!match)
|
|
2119
2156
|
return null;
|
|
2120
|
-
|
|
2121
|
-
const parsed = JSON.parse(match[1]);
|
|
2122
|
-
return isTypedStageHandoff(parsed) ? parsed : null;
|
|
2123
|
-
} catch {
|
|
2124
|
-
return null;
|
|
2125
|
-
}
|
|
2157
|
+
return { command: match[1], cardId: decodeURIComponent(match[2]) };
|
|
2126
2158
|
}
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2159
|
+
var TAG3 = "http";
|
|
2160
|
+
var init_http_server = __esm(() => {
|
|
2161
|
+
init_log();
|
|
2162
|
+
});
|
|
2163
|
+
|
|
2164
|
+
// src/auto-merge.ts
|
|
2165
|
+
function decideAutoMergeAction(input) {
|
|
2166
|
+
const { ciStatus, headSha, reviewedSha, config } = input;
|
|
2167
|
+
if (!config.enabled)
|
|
2168
|
+
return "wait";
|
|
2169
|
+
if (config.requireGreenCi) {
|
|
2170
|
+
if (ciStatus === "failure")
|
|
2171
|
+
return "stamp-failure";
|
|
2172
|
+
if (ciStatus !== "success")
|
|
2173
|
+
return "wait";
|
|
2142
2174
|
}
|
|
2143
|
-
|
|
2175
|
+
if (config.reReviewOnBranchChange && reviewedSha && headSha && reviewedSha !== headSha) {
|
|
2176
|
+
return "rereview";
|
|
2177
|
+
}
|
|
2178
|
+
return "merge";
|
|
2144
2179
|
}
|
|
2145
|
-
function
|
|
2146
|
-
const
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
"**Decisions you must respect:**",
|
|
2156
|
-
decisions,
|
|
2157
|
-
"",
|
|
2158
|
-
`**What you need to do with it:** ${handoff.nextStageNeeds}`
|
|
2159
|
-
].join(`
|
|
2160
|
-
`);
|
|
2180
|
+
async function stampCiFailure(client, card) {
|
|
2181
|
+
const existing = card.description || "";
|
|
2182
|
+
if (existing.includes("CI checks failed"))
|
|
2183
|
+
return;
|
|
2184
|
+
const sep = existing ? `
|
|
2185
|
+
` : "";
|
|
2186
|
+
const ts = new Date().toISOString();
|
|
2187
|
+
await client.updateCard(card.id, {
|
|
2188
|
+
description: `${existing}${sep}CI checks failed at ${ts}`
|
|
2189
|
+
});
|
|
2161
2190
|
}
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2191
|
+
async function removeApprovedLabel(client, card, resolvedLabels, approvedLabel) {
|
|
2192
|
+
const name = approvedLabel.toLowerCase();
|
|
2193
|
+
const obj = resolvedLabels.find((l) => l.name.toLowerCase() === name);
|
|
2194
|
+
if (obj)
|
|
2195
|
+
await client.removeLabelFromCard(card.id, obj.id);
|
|
2196
|
+
}
|
|
2197
|
+
async function attemptAutoMerge(deps) {
|
|
2198
|
+
const { client, card, resolvedLabels, prUrl, cwd, provider, config } = deps;
|
|
2199
|
+
const autoMerge = config.review.autoMerge;
|
|
2200
|
+
if (!autoMerge.enabled || provider !== "github")
|
|
2201
|
+
return;
|
|
2202
|
+
const { ciStatus, headSha } = await getPrStatus(prUrl, cwd, provider);
|
|
2203
|
+
const reviewedSha = extractReviewedSha(card.description ?? null);
|
|
2204
|
+
const action = decideAutoMergeAction({
|
|
2205
|
+
ciStatus,
|
|
2206
|
+
headSha,
|
|
2207
|
+
reviewedSha,
|
|
2208
|
+
config: autoMerge
|
|
2209
|
+
});
|
|
2210
|
+
switch (action) {
|
|
2211
|
+
case "wait":
|
|
2212
|
+
log.debug(TAG4, `#${card.short_id} waiting (ci=${ciStatus})`);
|
|
2213
|
+
return;
|
|
2214
|
+
case "stamp-failure":
|
|
2215
|
+
log.info(TAG4, `#${card.short_id} CI failed — flagging for human`);
|
|
2216
|
+
await stampCiFailure(client, card);
|
|
2217
|
+
return;
|
|
2218
|
+
case "rereview":
|
|
2219
|
+
log.info(TAG4, `#${card.short_id} branch changed since review — re-reviewing`);
|
|
2220
|
+
await removeApprovedLabel(client, card, resolvedLabels, config.review.approvedLabel);
|
|
2221
|
+
return;
|
|
2222
|
+
case "merge":
|
|
2223
|
+
log.info(TAG4, `#${card.short_id} auto-merging (${autoMerge.strategy})`);
|
|
2224
|
+
await mergePullRequest(prUrl, cwd, provider, autoMerge.strategy, autoMerge.deleteBranch);
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
var TAG4 = "auto-merge";
|
|
2229
|
+
var init_auto_merge = __esm(() => {
|
|
2230
|
+
init_git_pr();
|
|
2231
|
+
init_log();
|
|
2184
2232
|
});
|
|
2185
2233
|
|
|
2186
2234
|
// src/pm.ts
|
|
@@ -2506,9 +2554,24 @@ function extractBranchFromDescription(description) {
|
|
|
2506
2554
|
}
|
|
2507
2555
|
return branch;
|
|
2508
2556
|
}
|
|
2557
|
+
function qualifiesForAutoReview(description) {
|
|
2558
|
+
return Boolean(extractBranchFromDescription(description) || extractPrUrl(description ?? null));
|
|
2559
|
+
}
|
|
2560
|
+
async function resolveReviewBranch(description, cwd) {
|
|
2561
|
+
const fromLine = extractBranchFromDescription(description);
|
|
2562
|
+
if (fromLine)
|
|
2563
|
+
return { kind: "branch", branch: fromLine };
|
|
2564
|
+
const prUrl = extractPrUrl(description ?? null);
|
|
2565
|
+
if (!prUrl)
|
|
2566
|
+
return { kind: "none" };
|
|
2567
|
+
const provider = detectGitProvider(cwd);
|
|
2568
|
+
const resolved = await resolvePrHeadBranch(prUrl, cwd, provider);
|
|
2569
|
+
return resolved;
|
|
2570
|
+
}
|
|
2509
2571
|
var TAG7 = "review-worktree";
|
|
2510
2572
|
var init_review_worktree = __esm(() => {
|
|
2511
2573
|
init_dist();
|
|
2574
|
+
init_git_pr();
|
|
2512
2575
|
init_log();
|
|
2513
2576
|
init_pm();
|
|
2514
2577
|
init_worktree();
|
|
@@ -6409,40 +6472,30 @@ class ReviewWorker {
|
|
|
6409
6472
|
costCents: 0,
|
|
6410
6473
|
numTurns: 0
|
|
6411
6474
|
});
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
if (!resolved) {
|
|
6423
|
-
log.info(this.tag, `No local changes found for #${card.short_id} — marking for human review (staying in Review)`);
|
|
6424
|
-
await addLabelByName(this.client, card, NEED_REVIEW_LABEL, NEED_REVIEW_LABEL_COLOR);
|
|
6425
|
-
return;
|
|
6426
|
-
}
|
|
6427
|
-
log.info(this.tag, `Found local changes via ${resolved.source} for #${card.short_id}`);
|
|
6428
|
-
localDiff = resolved.diff;
|
|
6429
|
-
}
|
|
6430
|
-
if (this.branchName) {
|
|
6431
|
-
log.info(this.tag, `Review branch: ${this.branchName}`);
|
|
6475
|
+
const repoRoot = execFileSync10("git", ["rev-parse", "--show-toplevel"], {
|
|
6476
|
+
encoding: "utf-8",
|
|
6477
|
+
timeout: 5000
|
|
6478
|
+
}).trim();
|
|
6479
|
+
const resolution = await resolveReviewBranch(card.description, repoRoot);
|
|
6480
|
+
if (resolution.kind !== "branch") {
|
|
6481
|
+
const why = resolution.kind === "skip" ? resolution.reason : "no branch or PR reference";
|
|
6482
|
+
log.info(this.tag, `#${card.short_id} not auto-reviewable (${why}) — marking for human review (staying in Review)`);
|
|
6483
|
+
await addLabelByName(this.client, card, NEED_REVIEW_LABEL, NEED_REVIEW_LABEL_COLOR);
|
|
6484
|
+
return;
|
|
6432
6485
|
}
|
|
6486
|
+
this.branchName = resolution.branch;
|
|
6487
|
+
log.info(this.tag, `Review branch: ${this.branchName}`);
|
|
6433
6488
|
const { session: reviewSession } = await this.client.startAgentSession(card.id, {
|
|
6434
6489
|
agentIdentifier: agentIdentifier(this.id),
|
|
6435
6490
|
agentName: `${AGENT_NAME} (Review)`,
|
|
6436
6491
|
agentId: this.agentId,
|
|
6437
6492
|
status: "working",
|
|
6438
|
-
currentTask:
|
|
6493
|
+
currentTask: "Setting up review worktree",
|
|
6439
6494
|
progressPercent: 5
|
|
6440
6495
|
});
|
|
6441
6496
|
this.sessionId = reviewSession && typeof reviewSession === "object" && "id" in reviewSession ? reviewSession.id ?? null : null;
|
|
6442
6497
|
const labelPromise = addLabelByName(this.client, card, "agent", "#8b5cf6");
|
|
6443
|
-
|
|
6444
|
-
this.worktreePath = checkoutExistingBranch(this.config.worktree.basePath, this.branchName);
|
|
6445
|
-
}
|
|
6498
|
+
this.worktreePath = checkoutExistingBranch(this.config.worktree.basePath, this.branchName);
|
|
6446
6499
|
await labelPromise;
|
|
6447
6500
|
if (this.aborted)
|
|
6448
6501
|
return;
|
|
@@ -6453,47 +6506,41 @@ class ReviewWorker {
|
|
|
6453
6506
|
}
|
|
6454
6507
|
const port = this.reviewPort;
|
|
6455
6508
|
const cwd = this.worktreePath;
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
throw new DevServerReadinessError(`dev server failed to start (${devCmd}): ${devServerSpawnError.message}`);
|
|
6476
|
-
}
|
|
6477
|
-
await waitForDevServer(this.devServerProcess, 30000);
|
|
6478
|
-
await probeDevServer(port);
|
|
6479
|
-
log.info(this.tag, `Dev server ready on port ${port}`);
|
|
6480
|
-
await this.client.updateAgentProgress(card.id, {
|
|
6481
|
-
agentIdentifier: agentIdentifier(this.id),
|
|
6482
|
-
agentName: `${AGENT_NAME} (Review)`,
|
|
6483
|
-
status: "working",
|
|
6484
|
-
currentTask: "Reviewing changes",
|
|
6485
|
-
progressPercent: 15
|
|
6486
|
-
});
|
|
6509
|
+
log.info(this.tag, `Starting dev server on port ${port}...`);
|
|
6510
|
+
const [devCmd, devArgs] = spawnRunArgs("dev", "--port", String(port));
|
|
6511
|
+
this.devServerProcess = spawnInGroup(devCmd, devArgs, {
|
|
6512
|
+
cwd,
|
|
6513
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
6514
|
+
});
|
|
6515
|
+
let devServerSpawnError = null;
|
|
6516
|
+
this.devServerProcess.once("error", (err) => {
|
|
6517
|
+
devServerSpawnError = err;
|
|
6518
|
+
});
|
|
6519
|
+
await this.client.updateAgentProgress(card.id, {
|
|
6520
|
+
agentIdentifier: agentIdentifier(this.id),
|
|
6521
|
+
agentName: `${AGENT_NAME} (Review)`,
|
|
6522
|
+
status: "waiting",
|
|
6523
|
+
currentTask: `Starting dev server on port ${port}…`,
|
|
6524
|
+
progressPercent: 10
|
|
6525
|
+
});
|
|
6526
|
+
if (devServerSpawnError) {
|
|
6527
|
+
throw new DevServerReadinessError(`dev server failed to start (${devCmd}): ${devServerSpawnError.message}`);
|
|
6487
6528
|
}
|
|
6529
|
+
await waitForDevServer(this.devServerProcess, 30000);
|
|
6530
|
+
await probeDevServer(port);
|
|
6531
|
+
log.info(this.tag, `Dev server ready on port ${port}`);
|
|
6532
|
+
await this.client.updateAgentProgress(card.id, {
|
|
6533
|
+
agentIdentifier: agentIdentifier(this.id),
|
|
6534
|
+
agentName: `${AGENT_NAME} (Review)`,
|
|
6535
|
+
status: "working",
|
|
6536
|
+
currentTask: "Reviewing changes",
|
|
6537
|
+
progressPercent: 15
|
|
6538
|
+
});
|
|
6488
6539
|
if (this.aborted)
|
|
6489
6540
|
return;
|
|
6490
6541
|
let diff = "";
|
|
6491
6542
|
try {
|
|
6492
|
-
|
|
6493
|
-
diff = localDiff ?? "";
|
|
6494
|
-
} else {
|
|
6495
|
-
diff = execFileSync10("git", ["diff", `origin/${this.config.worktree.baseBranch}..HEAD`], { cwd, encoding: "utf-8", timeout: 30000 });
|
|
6496
|
-
}
|
|
6543
|
+
diff = execFileSync10("git", ["diff", `origin/${this.config.worktree.baseBranch}..HEAD`], { cwd, encoding: "utf-8", timeout: 30000 });
|
|
6497
6544
|
} catch {
|
|
6498
6545
|
diff = "(unable to retrieve diff)";
|
|
6499
6546
|
}
|
|
@@ -6545,9 +6592,7 @@ class ReviewWorker {
|
|
|
6545
6592
|
this.state = "completing";
|
|
6546
6593
|
await this.recordPhase("completing");
|
|
6547
6594
|
log.info(this.tag, `Claude review finished for #${card.short_id}`);
|
|
6548
|
-
|
|
6549
|
-
this.killDevServer();
|
|
6550
|
-
}
|
|
6595
|
+
this.killDevServer();
|
|
6551
6596
|
const result = parseReviewOutput(stdout);
|
|
6552
6597
|
log.info(this.tag, `Review verdict: ${result.verdict} (${result.findings.length} finding(s))`);
|
|
6553
6598
|
await this.client.updateAgentProgress(card.id, {
|
|
@@ -6799,50 +6844,6 @@ class ReviewWorker {
|
|
|
6799
6844
|
log.debug(this.tag, "Killed dev server group");
|
|
6800
6845
|
}
|
|
6801
6846
|
}
|
|
6802
|
-
resolveLocalChanges(repoRoot, shortId) {
|
|
6803
|
-
try {
|
|
6804
|
-
const localChanges = execFileSync10("git", ["diff", "HEAD"], {
|
|
6805
|
-
cwd: repoRoot,
|
|
6806
|
-
encoding: "utf-8",
|
|
6807
|
-
timeout: 5000
|
|
6808
|
-
});
|
|
6809
|
-
if (localChanges) {
|
|
6810
|
-
return { diff: localChanges, source: "uncommitted changes" };
|
|
6811
|
-
}
|
|
6812
|
-
} catch {
|
|
6813
|
-
log.warn(this.tag, "Failed to check uncommitted changes");
|
|
6814
|
-
}
|
|
6815
|
-
try {
|
|
6816
|
-
const matchingCommits = execFileSync10("git", ["log", "--format=%H", "-20", `--grep=#${shortId}`], { cwd: repoRoot, encoding: "utf-8", timeout: 1e4 }).trim();
|
|
6817
|
-
if (matchingCommits) {
|
|
6818
|
-
const hashes = matchingCommits.split(`
|
|
6819
|
-
`).filter((h) => /^[0-9a-f]{4,40}$/i.test(h));
|
|
6820
|
-
if (hashes.length === 0)
|
|
6821
|
-
return null;
|
|
6822
|
-
log.info(this.tag, `Found ${hashes.length} commit(s) referencing #${shortId}`);
|
|
6823
|
-
const diffs = [];
|
|
6824
|
-
for (const hash of hashes) {
|
|
6825
|
-
try {
|
|
6826
|
-
const commitDiff = execFileSync10("git", ["diff", `${hash}~1..${hash}`], { cwd: repoRoot, encoding: "utf-8", timeout: 30000 });
|
|
6827
|
-
if (commitDiff)
|
|
6828
|
-
diffs.push(commitDiff);
|
|
6829
|
-
} catch {
|
|
6830
|
-
log.warn(this.tag, `Failed to diff commit ${hash}`);
|
|
6831
|
-
}
|
|
6832
|
-
}
|
|
6833
|
-
if (diffs.length > 0) {
|
|
6834
|
-
return {
|
|
6835
|
-
diff: diffs.join(`
|
|
6836
|
-
`),
|
|
6837
|
-
source: `${diffs.length} commit(s) matching #${shortId}`
|
|
6838
|
-
};
|
|
6839
|
-
}
|
|
6840
|
-
}
|
|
6841
|
-
} catch {
|
|
6842
|
-
log.warn(this.tag, "Failed to search recent commits");
|
|
6843
|
-
}
|
|
6844
|
-
return null;
|
|
6845
|
-
}
|
|
6846
6847
|
cleanup() {
|
|
6847
6848
|
if (this.timeoutTimer) {
|
|
6848
6849
|
clearTimeout(this.timeoutTimer);
|
|
@@ -9540,8 +9541,8 @@ class Reconciler {
|
|
|
9540
9541
|
log.debug(TAG36, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
|
|
9541
9542
|
continue;
|
|
9542
9543
|
}
|
|
9543
|
-
if (mode === "review" && !
|
|
9544
|
-
log.debug(TAG36, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
|
|
9544
|
+
if (mode === "review" && !qualifiesForAutoReview(card.description)) {
|
|
9545
|
+
log.debug(TAG36, `Skipping #${card.short_id} — no branch or PR reference (not qualified for auto-review)`);
|
|
9545
9546
|
continue;
|
|
9546
9547
|
}
|
|
9547
9548
|
log.info(TAG36, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
|
|
@@ -10581,8 +10582,12 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
|
10581
10582
|
log.debug(TAG40, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
|
|
10582
10583
|
return;
|
|
10583
10584
|
}
|
|
10584
|
-
if (mode === "review" &&
|
|
10585
|
-
log.
|
|
10585
|
+
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
10586
|
+
log.debug(TAG40, `Card #${card.short_id} has "${NEED_REVIEW_LABEL}" label (needs human) — skipping review`);
|
|
10587
|
+
return;
|
|
10588
|
+
}
|
|
10589
|
+
if (mode === "review" && !qualifiesForAutoReview(card.description)) {
|
|
10590
|
+
log.info(TAG40, `Card #${card.short_id} has no branch or PR reference — skipping auto-review`);
|
|
10586
10591
|
return;
|
|
10587
10592
|
}
|
|
10588
10593
|
await pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
@@ -10604,6 +10609,7 @@ var init_src = __esm(() => {
|
|
|
10604
10609
|
init_startup_banner();
|
|
10605
10610
|
init_state_store();
|
|
10606
10611
|
init_stream_parser_selftest();
|
|
10612
|
+
init_types();
|
|
10607
10613
|
init_unblock();
|
|
10608
10614
|
init_watcher();
|
|
10609
10615
|
init_worktree_gc();
|