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