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