@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/dist/cli.js +527 -69
- package/dist/index.js +660 -228
- package/package.json +2 -2
- package/src/ci-failure.ts +10 -5
- package/src/exec-types.ts +5 -7
- package/src/git-diff-stat.ts +2 -1
- package/src/git-pr.ts +59 -25
- package/src/index.ts +1 -0
- package/src/pm.ts +8 -3
- package/src/revert-guard.ts +9 -2
- package/src/run-containment.ts +1292 -0
- package/src/sdk-agent-runner.ts +19 -0
- package/src/verification.ts +84 -7
- package/src/worktree.ts +149 -57
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gethmy/harness",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
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(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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;
|
package/src/git-diff-stat.ts
CHANGED
|
@@ -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(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
[
|
|
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(
|
|
987
|
-
|
|
988
|
-
|
|
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
|
-
[
|
|
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(
|
|
1029
|
-
|
|
1030
|
-
|
|
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(
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
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
|
-
[
|
|
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(
|
|
20
|
-
|
|
21
|
-
|
|
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
|
}
|
package/src/revert-guard.ts
CHANGED
|
@@ -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
|
-
[
|
|
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
|