@gethmy/harness 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/harness",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Execution motor for Harmony playbook stages. Runs exactly one stage per invocation: worktree, role-separated subagents, held oracle, gate evidence. It never routes, never judges, never pushes.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "@anthropic-ai/claude-agent-sdk": "^0.3.178",
58
- "@gethmy/mcp": "3.1.0",
58
+ "@gethmy/mcp": "3.2.0",
59
59
  "@supabase/supabase-js": "2.95.3"
60
60
  },
61
61
  "devDependencies": {
package/src/ci-failure.ts CHANGED
@@ -2,6 +2,7 @@ import { execFile, execFileSync } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  import type { GitProvider } from "./git-pr.js";
4
4
  import { log } from "./log.js";
5
+ import { GIT_NO_HOOKS } from "./run-containment.js";
5
6
 
6
7
  /**
7
8
  * Reading a red CI in enough detail to act on it (card #981).
@@ -137,11 +138,15 @@ export function isPrOnOrigin(prUrl: string, cwd: string): boolean {
137
138
  const prSlug = githubSlug(prUrl);
138
139
  if (!prSlug) return false;
139
140
  try {
140
- const remote = execFileSync("git", ["remote", "get-url", "origin"], {
141
- cwd,
142
- encoding: "utf-8",
143
- stdio: ["ignore", "pipe", "pipe"],
144
- }).trim();
141
+ const remote = execFileSync(
142
+ "git",
143
+ [...GIT_NO_HOOKS, "remote", "get-url", "origin"],
144
+ {
145
+ cwd,
146
+ encoding: "utf-8",
147
+ stdio: ["ignore", "pipe", "pipe"],
148
+ },
149
+ ).trim();
145
150
  return githubSlug(remote) === prSlug;
146
151
  } catch {
147
152
  return false;
package/src/exec-types.ts CHANGED
@@ -58,13 +58,11 @@ export interface VerificationConfig {
58
58
  * until then this is the fallback for all cards.)
59
59
  */
60
60
  model: string;
61
- /**
62
- * `--setting-sources` value for lean spawns (review, auto-fix, deep-review)
63
- * that don't need the project CLAUDE.md / @-imports (~23KB). `"local,user"`
64
- * keeps the local-scoped harmony MCP server + user settings while dropping
65
- * the project docs. Empty string omits the flag (full default behaviour).
66
- */
67
- leanSettingSources: string;
61
+ // `leanSettingSources` was removed in #988. The three spawns that read it —
62
+ // review, auto-fix and deep-review — are now pinned to the containment's
63
+ // own `settingSources`, so an operator value here could only ever widen a
64
+ // security boundary. Removed rather than ignored, so a config that sets it
65
+ // fails to typecheck instead of appearing to work.
68
66
  };
69
67
  worktree: {
70
68
  baseBranch: string;
@@ -1,5 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { log } from "./log.js";
3
+ import { GIT_NO_HOOKS } from "./run-containment.js";
3
4
 
4
5
  const TAG = "git-diff-stat";
5
6
 
@@ -134,7 +135,7 @@ export function captureDiffStat(
134
135
  try {
135
136
  const raw = execFileSync(
136
137
  "git",
137
- ["diff", "--numstat", `${baseBranch}...HEAD`],
138
+ [...GIT_NO_HOOKS, "diff", "--numstat", `${baseBranch}...HEAD`],
138
139
  { cwd: worktreePath, encoding: "utf-8", timeout: 30_000 },
139
140
  );
140
141
  return parseNumstat(raw, maxFiles);
package/src/git-pr.ts CHANGED
@@ -4,6 +4,7 @@ import type { Card } from "@harmony/shared";
4
4
  import { SAFE_GIT_REF_PATTERN } from "@harmony/shared";
5
5
  import type { MergeStrategy, WorktreeConfig } from "./exec-types.js";
6
6
  import { log } from "./log.js";
7
+ import { GIT_NO_HOOKS } from "./run-containment.js";
7
8
 
8
9
  // Lazy + memoised: promisify(execFile) has no reason to run at module load.
9
10
  // git-pr.ts sits in the motor's public barrel (@gethmy/harness `export *`), so
@@ -33,10 +34,14 @@ export type GitProvider =
33
34
 
34
35
  export function detectGitProvider(cwd?: string): GitProvider {
35
36
  try {
36
- const url = execFileSync("git", ["remote", "get-url", "origin"], {
37
- cwd,
38
- encoding: "utf-8",
39
- }).trim();
37
+ const url = execFileSync(
38
+ "git",
39
+ [...GIT_NO_HOOKS, "remote", "get-url", "origin"],
40
+ {
41
+ cwd,
42
+ encoding: "utf-8",
43
+ },
44
+ ).trim();
40
45
 
41
46
  if (url.includes("github.com")) return "github";
42
47
  if (url.includes("dev.azure.com") || url.includes("visualstudio.com"))
@@ -671,7 +676,7 @@ export async function mergePullRequest(
671
676
 
672
677
  export function getHeadSha(cwd: string): string | null {
673
678
  try {
674
- return execFileSync("git", ["rev-parse", "HEAD"], {
679
+ return execFileSync("git", [...GIT_NO_HOOKS, "rev-parse", "HEAD"], {
675
680
  cwd,
676
681
  encoding: "utf-8",
677
682
  }).trim();
@@ -947,7 +952,13 @@ export function remoteBranchExists(branchName: string, cwd: string): boolean {
947
952
  try {
948
953
  execFileSync(
949
954
  "git",
950
- ["ls-remote", "--exit-code", "origin", `refs/heads/${branchName}`],
955
+ [
956
+ ...GIT_NO_HOOKS,
957
+ "ls-remote",
958
+ "--exit-code",
959
+ "origin",
960
+ `refs/heads/${branchName}`,
961
+ ],
951
962
  { cwd, stdio: "pipe" },
952
963
  );
953
964
  return true;
@@ -965,13 +976,13 @@ export function pushBranch(branchName: string, cwd: string): void {
965
976
  // lets a concurrent update slip through. Fetch + pin the expected SHA.
966
977
  let expectedSha: string | null = null;
967
978
  try {
968
- execFileSync("git", ["fetch", "origin", branchName], {
979
+ execFileSync("git", [...GIT_NO_HOOKS, "fetch", "origin", branchName], {
969
980
  cwd,
970
981
  stdio: "pipe",
971
982
  });
972
983
  expectedSha = execFileSync(
973
984
  "git",
974
- ["rev-parse", `refs/remotes/origin/${branchName}`],
985
+ [...GIT_NO_HOOKS, "rev-parse", `refs/remotes/origin/${branchName}`],
975
986
  { cwd, encoding: "utf-8" },
976
987
  ).trim();
977
988
  } catch (err) {
@@ -983,12 +994,16 @@ export function pushBranch(branchName: string, cwd: string): void {
983
994
  const lease = expectedSha
984
995
  ? `--force-with-lease=refs/heads/${branchName}:${expectedSha}`
985
996
  : "--force-with-lease";
986
- execFileSync("git", ["push", lease, "-u", "origin", branchName], {
987
- cwd,
988
- stdio: "pipe",
989
- });
997
+ execFileSync(
998
+ "git",
999
+ [...GIT_NO_HOOKS, "push", lease, "-u", "origin", branchName],
1000
+ {
1001
+ cwd,
1002
+ stdio: "pipe",
1003
+ },
1004
+ );
990
1005
  } else {
991
- execFileSync("git", ["push", "-u", "origin", branchName], {
1006
+ execFileSync("git", [...GIT_NO_HOOKS, "push", "-u", "origin", branchName], {
992
1007
  cwd,
993
1008
  stdio: "pipe",
994
1009
  });
@@ -1009,7 +1024,7 @@ export function renameRemoteBranch(
1009
1024
  if (oldRef === newRef) return;
1010
1025
  let sha: string;
1011
1026
  try {
1012
- sha = execFileSync("git", ["rev-parse", "HEAD"], {
1027
+ sha = execFileSync("git", [...GIT_NO_HOOKS, "rev-parse", "HEAD"], {
1013
1028
  cwd,
1014
1029
  encoding: "utf-8",
1015
1030
  }).trim();
@@ -1021,14 +1036,24 @@ export function renameRemoteBranch(
1021
1036
  log.info(TAG, `Renaming remote ${oldRef} → ${newRef}`);
1022
1037
  execFileSync(
1023
1038
  "git",
1024
- ["push", "origin", `${sha}:refs/heads/${newRef}`, "--force-with-lease"],
1039
+ [
1040
+ ...GIT_NO_HOOKS,
1041
+ "push",
1042
+ "origin",
1043
+ `${sha}:refs/heads/${newRef}`,
1044
+ "--force-with-lease",
1045
+ ],
1025
1046
  { cwd, stdio: "pipe" },
1026
1047
  );
1027
1048
  try {
1028
- execFileSync("git", ["push", "origin", `:refs/heads/${oldRef}`], {
1029
- cwd,
1030
- stdio: "pipe",
1031
- });
1049
+ execFileSync(
1050
+ "git",
1051
+ [...GIT_NO_HOOKS, "push", "origin", `:refs/heads/${oldRef}`],
1052
+ {
1053
+ cwd,
1054
+ stdio: "pipe",
1055
+ },
1056
+ );
1032
1057
  } catch (err) {
1033
1058
  log.warn(
1034
1059
  TAG,
@@ -1036,7 +1061,7 @@ export function renameRemoteBranch(
1036
1061
  );
1037
1062
  }
1038
1063
  try {
1039
- execFileSync("git", ["branch", "-m", oldRef, newRef], {
1064
+ execFileSync("git", [...GIT_NO_HOOKS, "branch", "-m", oldRef, newRef], {
1040
1065
  cwd,
1041
1066
  stdio: "pipe",
1042
1067
  });
@@ -1055,10 +1080,14 @@ export function getBranchWebUrl(
1055
1080
  cwd: string,
1056
1081
  ): string | null {
1057
1082
  try {
1058
- const remoteUrl = execFileSync("git", ["remote", "get-url", "origin"], {
1059
- cwd,
1060
- encoding: "utf-8",
1061
- }).trim();
1083
+ const remoteUrl = execFileSync(
1084
+ "git",
1085
+ [...GIT_NO_HOOKS, "remote", "get-url", "origin"],
1086
+ {
1087
+ cwd,
1088
+ encoding: "utf-8",
1089
+ },
1090
+ ).trim();
1062
1091
  const encoded = branchName.split("/").map(encodeURIComponent).join("/");
1063
1092
  if (/github\.com[:/]([^/]+)\/([^/.]+)/.test(remoteUrl)) {
1064
1093
  const m = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/);
@@ -1126,7 +1155,12 @@ export function createPullRequest(
1126
1155
  try {
1127
1156
  commitLog = execFileSync(
1128
1157
  "git",
1129
- ["log", "--oneline", `origin/${config.worktree.baseBranch}..HEAD`],
1158
+ [
1159
+ ...GIT_NO_HOOKS,
1160
+ "log",
1161
+ "--oneline",
1162
+ `origin/${config.worktree.baseBranch}..HEAD`,
1163
+ ],
1130
1164
  { cwd: worktreePath, encoding: "utf-8" },
1131
1165
  ).trim();
1132
1166
  } catch {
package/src/index.ts CHANGED
@@ -34,6 +34,7 @@ export * from "./project-type.js";
34
34
  export * from "./repair-sandbox.js";
35
35
  export * from "./revert-guard.js";
36
36
  export * from "./review-types.js";
37
+ export * from "./run-containment.js";
37
38
  export * from "./run-sizing.js";
38
39
  export * from "./runner.js";
39
40
  export * from "./sdk-agent-runner.js";
package/src/pm.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { log } from "./log.js";
4
+ import { GIT_NO_HOOKS } from "./run-containment.js";
4
5
 
5
6
  const TAG = "pm";
6
7
 
@@ -16,9 +17,13 @@ export function detectPackageManager(): PackageManager {
16
17
 
17
18
  let repoRoot: string;
18
19
  try {
19
- repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
20
- encoding: "utf-8",
21
- }).trim();
20
+ repoRoot = execFileSync(
21
+ "git",
22
+ [...GIT_NO_HOOKS, "rev-parse", "--show-toplevel"],
23
+ {
24
+ encoding: "utf-8",
25
+ },
26
+ ).trim();
22
27
  } catch {
23
28
  repoRoot = process.cwd();
24
29
  }
@@ -1,5 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { log } from "./log.js";
3
+ import { GIT_NO_HOOKS } from "./run-containment.js";
3
4
 
4
5
  const TAG = "revert-guard";
5
6
 
@@ -44,7 +45,7 @@ export function filterTestFiles(paths: string[]): string[] {
44
45
  */
45
46
  function refetchBase(worktreePath: string, baseBranch: string): void {
46
47
  try {
47
- execFileSync("git", ["fetch", "origin", baseBranch], {
48
+ execFileSync("git", [...GIT_NO_HOOKS, "fetch", "origin", baseBranch], {
48
49
  cwd: worktreePath,
49
50
  stdio: "pipe",
50
51
  });
@@ -69,7 +70,13 @@ export function listDeletedFilesAgainstBase(
69
70
  try {
70
71
  const out = execFileSync(
71
72
  "git",
72
- ["diff", "--diff-filter=D", "--name-only", `origin/${baseBranch}...HEAD`],
73
+ [
74
+ ...GIT_NO_HOOKS,
75
+ "diff",
76
+ "--diff-filter=D",
77
+ "--name-only",
78
+ `origin/${baseBranch}...HEAD`,
79
+ ],
73
80
  { cwd: worktreePath, encoding: "utf-8" },
74
81
  );
75
82
  return out