@gethmy/agent 1.18.1 → 1.19.0

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