@gethmy/agent 1.39.0 → 1.40.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/README.md +1 -1
- package/dist/cli.js +789 -120
- package/dist/index.js +498 -74
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -852,6 +852,9 @@ function buildGaveUpComment(maxAttempts, failures, pauseEnabled) {
|
|
|
852
852
|
function mergeAgentConfig(defaults, overrides) {
|
|
853
853
|
return mergeObjects(defaults, overrides);
|
|
854
854
|
}
|
|
855
|
+
function own(source, key) {
|
|
856
|
+
return Object.hasOwn(source, key) ? source[key] : undefined;
|
|
857
|
+
}
|
|
855
858
|
function isPlainObject(value) {
|
|
856
859
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
857
860
|
return false;
|
|
@@ -864,7 +867,7 @@ function mergeObjects(base, override) {
|
|
|
864
867
|
for (const key of Object.keys(base)) {
|
|
865
868
|
if (FORBIDDEN_KEYS.has(key))
|
|
866
869
|
continue;
|
|
867
|
-
result[key] = mergeValue(base[key], override
|
|
870
|
+
result[key] = mergeValue(base[key], own(override, key));
|
|
868
871
|
}
|
|
869
872
|
for (const key of Object.keys(override)) {
|
|
870
873
|
if (FORBIDDEN_KEYS.has(key))
|
|
@@ -903,7 +906,7 @@ function walk(base, override, path, found) {
|
|
|
903
906
|
if (FORBIDDEN_KEYS.has(key))
|
|
904
907
|
continue;
|
|
905
908
|
const baseValue = base[key];
|
|
906
|
-
const overrideValue = override
|
|
909
|
+
const overrideValue = own(override, key);
|
|
907
910
|
if (overrideValue === undefined)
|
|
908
911
|
continue;
|
|
909
912
|
const here = `${path}.${key}`;
|
|
@@ -948,7 +951,11 @@ function copy(value) {
|
|
|
948
951
|
}
|
|
949
952
|
var FORBIDDEN_KEYS;
|
|
950
953
|
var init_config_merge = __esm(() => {
|
|
951
|
-
FORBIDDEN_KEYS = new Set([
|
|
954
|
+
FORBIDDEN_KEYS = new Set([
|
|
955
|
+
"__proto__",
|
|
956
|
+
"constructor",
|
|
957
|
+
"prototype"
|
|
958
|
+
]);
|
|
952
959
|
});
|
|
953
960
|
|
|
954
961
|
// src/config-validation.ts
|
|
@@ -963,8 +970,10 @@ __export(exports_config_validation, {
|
|
|
963
970
|
validateBudgetConfig: () => validateBudgetConfig,
|
|
964
971
|
validateAutoMergeConfig: () => validateAutoMergeConfig,
|
|
965
972
|
validateAndListColumns: () => validateAndListColumns,
|
|
973
|
+
repoArmedGatesOf: () => repoArmedGatesOf,
|
|
966
974
|
isLoopbackBindAddr: () => isLoopbackBindAddr,
|
|
967
975
|
findRemovedConfigKeys: () => findRemovedConfigKeys,
|
|
976
|
+
REPO_ARMABLE_GATES: () => REPO_ARMABLE_GATES,
|
|
968
977
|
REMOVED_CONFIG_KEYS: () => REMOVED_CONFIG_KEYS,
|
|
969
978
|
ConfigValidationError: () => ConfigValidationError
|
|
970
979
|
});
|
|
@@ -979,7 +988,7 @@ function findRemovedConfigKeys(rawConfig) {
|
|
|
979
988
|
cursor = undefined;
|
|
980
989
|
break;
|
|
981
990
|
}
|
|
982
|
-
cursor = cursor
|
|
991
|
+
cursor = own(cursor, segment);
|
|
983
992
|
}
|
|
984
993
|
if (cursor !== undefined) {
|
|
985
994
|
found.push(`${path} is no longer used (#988) — ${note}. Remove it.`);
|
|
@@ -1012,9 +1021,49 @@ function validateVerificationConfig(config) {
|
|
|
1012
1021
|
- `)}`, issues);
|
|
1013
1022
|
}
|
|
1014
1023
|
}
|
|
1015
|
-
function
|
|
1024
|
+
function repoArmedGatesOf(suppliedPaths, operatorAgent, merged) {
|
|
1025
|
+
const armed = new Set;
|
|
1026
|
+
for (const gate of Object.keys(REPO_ARMABLE_GATES)) {
|
|
1027
|
+
if (!suppliedPaths.has(gate))
|
|
1028
|
+
continue;
|
|
1029
|
+
if (ownValueAt(operatorAgent, gate))
|
|
1030
|
+
continue;
|
|
1031
|
+
if (ownValueAt(merged, gate) !== true)
|
|
1032
|
+
continue;
|
|
1033
|
+
armed.add(gate);
|
|
1034
|
+
}
|
|
1035
|
+
return armed;
|
|
1036
|
+
}
|
|
1037
|
+
function ownValueAt(root, dotted) {
|
|
1038
|
+
let cursor = root;
|
|
1039
|
+
for (const segment of dotted.split(".")) {
|
|
1040
|
+
if (cursor === null || typeof cursor !== "object" || !Object.hasOwn(cursor, segment)) {
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
cursor = cursor[segment];
|
|
1044
|
+
}
|
|
1045
|
+
return cursor;
|
|
1046
|
+
}
|
|
1047
|
+
function armedByRepoPin(gate) {
|
|
1048
|
+
if (REPO_ARMABLE_GATES[gate] === "tightening") {
|
|
1049
|
+
return `This repository's .hmy.json sets "${gate}", and your own config had that gate off — so the repository is what makes this rule apply at all. It is a tightening, so the repository may set it without your "allowRepoOverrides", and your own config was legal until you cloned this checkout. Two remedies: drop "${gate}" from this repository's .hmy.json, or give the key this message names a usable value in ~/.hmy/agent/config.json — which keeps the gate armed, as the repository asked, unless the pin supplies that key too (it is the top rung, and any key it supplied is named in the warnings above).`;
|
|
1050
|
+
}
|
|
1051
|
+
return `This repository's .hmy.json sets "${gate}", and your own config had it off — so the repository is what makes this rule apply at all. That is a loosening, accepted only because your ~/.hmy/agent/config.json sets "allowRepoOverrides": true, and your own config was legal until you cloned this checkout. Two remedies: drop "${gate}" from this repository's .hmy.json, or turn "allowRepoOverrides" off in your own config, which drops every repo-supplied loosening at once. To keep what the repository asked for instead, satisfy the rule this message opens with — unless this repository's .hmy.json supplies that key as well, in which case only the two remedies above can help, because the pin is the top rung.`;
|
|
1052
|
+
}
|
|
1053
|
+
function validateAutoMergeConfig(config, repoSuppliedPaths = NO_REPO_PATHS, repoArmedGates = NO_REPO_PATHS) {
|
|
1016
1054
|
const autoMerge = config.review.autoMerge;
|
|
1017
1055
|
const issues = [];
|
|
1056
|
+
const warnings = [];
|
|
1057
|
+
const armedRefusal = (gate, issue) => {
|
|
1058
|
+
issues.push(repoArmedGates.has(gate) ? `${issue}. ${armedByRepoPin(gate)}` : issue);
|
|
1059
|
+
};
|
|
1060
|
+
const inertCombination = (args) => {
|
|
1061
|
+
if (repoSuppliedPaths.has(args.gate)) {
|
|
1062
|
+
warnings.push(`${args.pin} is inert here: this repository's .hmy.json sets ${args.gate}, and ${args.pin} only runs behind it. The repository's tightening wins — your own config is not wrong, and nothing else is switched off.`);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
armedRefusal(args.pin, args.issue);
|
|
1066
|
+
};
|
|
1018
1067
|
const valid = ["squash", "merge", "rebase"];
|
|
1019
1068
|
const s = autoMerge.strategy;
|
|
1020
1069
|
if (!valid.includes(s)) {
|
|
@@ -1023,34 +1072,44 @@ function validateAutoMergeConfig(config) {
|
|
|
1023
1072
|
const repair = autoMerge.ciRepair;
|
|
1024
1073
|
if (repair.enabled) {
|
|
1025
1074
|
if (!autoMerge.enabled) {
|
|
1026
|
-
|
|
1075
|
+
inertCombination({
|
|
1076
|
+
pin: "review.autoMerge.ciRepair.enabled",
|
|
1077
|
+
gate: "review.autoMerge.enabled",
|
|
1078
|
+
issue: "review.autoMerge.ciRepair.enabled: needs review.autoMerge.enabled — a repair only runs on a card the daemon would merge itself"
|
|
1079
|
+
});
|
|
1027
1080
|
}
|
|
1028
1081
|
if (!autoMerge.requireGreenCi) {
|
|
1029
|
-
|
|
1082
|
+
armedRefusal("review.autoMerge.ciRepair.enabled", "review.autoMerge.ciRepair.enabled: needs review.autoMerge.requireGreenCi — without it a red CI never reaches the repair branch, so the loop can never run");
|
|
1030
1083
|
}
|
|
1031
1084
|
if (!Number.isInteger(repair.maxAttempts) || repair.maxAttempts < 1) {
|
|
1032
|
-
|
|
1085
|
+
armedRefusal("review.autoMerge.ciRepair.enabled", `review.autoMerge.ciRepair.maxAttempts: must be an integer >= 1 (got ${repair.maxAttempts}) — the cap is the safety property and has no opt-out`);
|
|
1033
1086
|
}
|
|
1034
1087
|
}
|
|
1035
1088
|
const patch = repair.patch;
|
|
1036
1089
|
if (patch?.enabled) {
|
|
1037
1090
|
if (!repair.enabled) {
|
|
1038
|
-
|
|
1091
|
+
inertCombination({
|
|
1092
|
+
pin: "review.autoMerge.ciRepair.patch.enabled",
|
|
1093
|
+
gate: "review.autoMerge.ciRepair.enabled",
|
|
1094
|
+
issue: "review.autoMerge.ciRepair.patch.enabled: needs review.autoMerge.ciRepair.enabled — the repair runs inside the loop that cap, park latch and ownership re-read belong to"
|
|
1095
|
+
});
|
|
1039
1096
|
}
|
|
1040
1097
|
if (!autoMerge.reReviewOnBranchChange) {
|
|
1041
|
-
|
|
1098
|
+
armedRefusal("review.autoMerge.ciRepair.patch.enabled", "review.autoMerge.ciRepair.patch.enabled: needs review.autoMerge.reReviewOnBranchChange — without it a machine-authored repair could merge with nothing having reviewed its diff");
|
|
1042
1099
|
}
|
|
1043
|
-
|
|
1100
|
+
for (const issue of sandboxConfigIssues({
|
|
1044
1101
|
image: patch.sandboxImage,
|
|
1045
1102
|
timeoutMs: patch.sandboxTimeoutMs,
|
|
1046
1103
|
imagePath: "review.autoMerge.ciRepair.patch.sandboxImage",
|
|
1047
1104
|
timeoutPath: "review.autoMerge.ciRepair.patch.sandboxTimeoutMs"
|
|
1048
|
-
}))
|
|
1105
|
+
})) {
|
|
1106
|
+
armedRefusal("review.autoMerge.ciRepair.patch.enabled", issue);
|
|
1107
|
+
}
|
|
1049
1108
|
if (!Number.isInteger(patch.maxTurns) || patch.maxTurns < 1) {
|
|
1050
|
-
|
|
1109
|
+
armedRefusal("review.autoMerge.ciRepair.patch.enabled", `review.autoMerge.ciRepair.patch.maxTurns: must be an integer >= 1 (got ${patch.maxTurns})`);
|
|
1051
1110
|
}
|
|
1052
1111
|
if (!(patch.maxBudgetUsd > 0)) {
|
|
1053
|
-
|
|
1112
|
+
armedRefusal("review.autoMerge.ciRepair.patch.enabled", `review.autoMerge.ciRepair.patch.maxBudgetUsd: must be greater than 0 (got ${patch.maxBudgetUsd})`);
|
|
1054
1113
|
}
|
|
1055
1114
|
}
|
|
1056
1115
|
const independent = autoMerge.independentReview;
|
|
@@ -1059,38 +1118,46 @@ function validateAutoMergeConfig(config) {
|
|
|
1059
1118
|
}
|
|
1060
1119
|
if (independent?.enabled) {
|
|
1061
1120
|
if (typeof independent.checkName !== "string" || !independent.checkName.trim()) {
|
|
1062
|
-
|
|
1121
|
+
armedRefusal("review.autoMerge.independentReview.enabled", "review.autoMerge.independentReview.checkName: must name a non-empty check — an unmatched name reads as no review at all and holds every merge");
|
|
1063
1122
|
}
|
|
1064
1123
|
if (typeof independent.label !== "string" || !independent.label.trim()) {
|
|
1065
|
-
|
|
1124
|
+
armedRefusal("review.autoMerge.independentReview.enabled", "review.autoMerge.independentReview.label: must name the PR label that requests a CI review");
|
|
1066
1125
|
}
|
|
1067
1126
|
const verdict = independent.verdict;
|
|
1068
1127
|
if (typeof verdict?.enabled !== "boolean") {
|
|
1069
|
-
|
|
1128
|
+
armedRefusal("review.autoMerge.independentReview.enabled", `review.autoMerge.independentReview.verdict.enabled: must be true or false (got ${JSON.stringify(verdict?.enabled)}) — an absent value would switch this safety gate off without saying so`);
|
|
1070
1129
|
}
|
|
1071
1130
|
if (typeof verdict?.checkName !== "string" || !verdict.checkName.trim()) {
|
|
1072
|
-
|
|
1131
|
+
armedRefusal("review.autoMerge.independentReview.enabled", "review.autoMerge.independentReview.verdict.checkName: must name a non-empty check even when verdict.enabled is false — the name keeps that check out of the collapsed CI verdict, so a blank one turns a review finding into a red build");
|
|
1073
1132
|
}
|
|
1074
1133
|
if (typeof independent.checkName === "string" && typeof verdict?.checkName === "string" && independent.checkName.trim().toLowerCase() === verdict.checkName.trim().toLowerCase() && independent.checkName.trim()) {
|
|
1075
|
-
|
|
1134
|
+
armedRefusal("review.autoMerge.independentReview.enabled", `review.autoMerge.independentReview.verdict.checkName: must not be the same check as independentReview.checkName ("${independent.checkName}") — one says a review RAN and the other says what it concluded, and each reader skips the other's check, so naming one check for both holds every merge`);
|
|
1076
1135
|
}
|
|
1077
1136
|
}
|
|
1078
1137
|
if (typeof autoMerge.interactive !== "boolean") {
|
|
1079
1138
|
issues.push(`review.autoMerge.interactive: must be true or false (got ${JSON.stringify(autoMerge.interactive)}) — an absent value would switch the interactive merge on or off without saying so`);
|
|
1080
1139
|
}
|
|
1140
|
+
if (autoMerge.interactive || autoMerge.enabled) {
|
|
1141
|
+
const driver = autoMerge.interactive ? "review.autoMerge.interactive" : "review.autoMerge.enabled";
|
|
1142
|
+
for (const named of REPO_UNSETTABLE_CHECK_NAMES) {
|
|
1143
|
+
if (!repoSuppliedPaths.has(named))
|
|
1144
|
+
continue;
|
|
1145
|
+
issues.push(`${named}: must not come from a repository's .hmy.json while ${driver} is on — the named check IS the independent review on that driver, and it is also excluded from the collapsed CI verdict, so a repository choosing the name chooses both what counts as having read the diff and which red check the merge gate stops seeing. Two remedies: set it in your own ~/.hmy/agent/config.json (an operator naming their own checks is untouched), or drop the key from this repository's .hmy.json. Turning off "allowRepoOverrides" in your own config also drops it, at the cost of every other repo-supplied setting.`);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1081
1148
|
if (autoMerge.interactive) {
|
|
1082
1149
|
if (independent?.enabled === false) {
|
|
1083
|
-
|
|
1150
|
+
armedRefusal("review.autoMerge.interactive", "review.autoMerge.independentReview.enabled: must be true while review.autoMerge.interactive is on — the interactive merge relaxes the Reviewed-SHA rule (no hmy card carries that marker), so the independent review is the only thing left that reads the diff, and nobody else is looking");
|
|
1084
1151
|
}
|
|
1085
1152
|
if (!autoMerge.reReviewOnBranchChange) {
|
|
1086
|
-
|
|
1153
|
+
armedRefusal("review.autoMerge.interactive", "review.autoMerge.reReviewOnBranchChange: must be true while review.autoMerge.interactive is on — with the independent review off it is the only rule left that holds an interactive merge, and the interactive driver reads no Reviewed-SHA of its own");
|
|
1087
1154
|
}
|
|
1088
1155
|
if (independent?.enabled && independent.verdict?.enabled === false) {
|
|
1089
|
-
|
|
1156
|
+
armedRefusal("review.autoMerge.interactive", "review.autoMerge.independentReview.verdict.enabled: must be true while review.autoMerge.interactive is on — the interactive driver reads no Reviewed-SHA of its own, so the verdict is the only remaining signal that a review approved the diff, and a green review check alone means the pass ran, not that it approved (#1018)");
|
|
1090
1157
|
}
|
|
1091
1158
|
const w = autoMerge.waitMinutes;
|
|
1092
1159
|
if (!Number.isInteger(w) || w < 1 || w > 55) {
|
|
1093
|
-
|
|
1160
|
+
armedRefusal("review.autoMerge.interactive", `review.autoMerge.waitMinutes: must be an integer between 1 and 55 (got ${w}) — an interactive agent session goes stale at about an hour, and a command that outlives its session reports into nothing`);
|
|
1094
1161
|
}
|
|
1095
1162
|
}
|
|
1096
1163
|
if (typeof autoMerge.findingsLoop !== "boolean") {
|
|
@@ -1102,16 +1169,21 @@ function validateAutoMergeConfig(config) {
|
|
|
1102
1169
|
}
|
|
1103
1170
|
if (autoMerge.findingsLoop) {
|
|
1104
1171
|
if (!autoMerge.interactive) {
|
|
1105
|
-
|
|
1172
|
+
inertCombination({
|
|
1173
|
+
pin: "review.autoMerge.findingsLoop",
|
|
1174
|
+
gate: "review.autoMerge.interactive",
|
|
1175
|
+
issue: "review.autoMerge.findingsLoop: needs review.autoMerge.interactive — the loop only runs on the interactive merge path, so armed alone it is inert rather than weaker"
|
|
1176
|
+
});
|
|
1106
1177
|
}
|
|
1107
1178
|
const findingAuthors = independent?.findingAuthors;
|
|
1108
1179
|
if (!Array.isArray(findingAuthors) || findingAuthors.filter((a) => typeof a === "string" && a.trim()).length === 0) {
|
|
1109
|
-
|
|
1180
|
+
armedRefusal("review.autoMerge.findingsLoop", "review.autoMerge.independentReview.findingAuthors: must name at least one login while review.autoMerge.findingsLoop is on — it is the only thing separating the review's findings from every other commenter's text, and an empty list parks every round");
|
|
1110
1181
|
}
|
|
1111
1182
|
}
|
|
1112
1183
|
if (issues.length > 0) {
|
|
1113
1184
|
throw new ConfigValidationError(`Invalid agent config — ${issues.join("; ")}`, issues);
|
|
1114
1185
|
}
|
|
1186
|
+
return warnings;
|
|
1115
1187
|
}
|
|
1116
1188
|
function isLoopbackBindAddr(addr) {
|
|
1117
1189
|
const value = addr.toLowerCase();
|
|
@@ -1333,8 +1405,9 @@ async function validateAndListColumns(client, projectId, config) {
|
|
|
1333
1405
|
}
|
|
1334
1406
|
return Array.from(new Set(names.filter(Boolean)));
|
|
1335
1407
|
}
|
|
1336
|
-
var ConfigValidationError, REMOVED_CONFIG_KEYS;
|
|
1408
|
+
var ConfigValidationError, REMOVED_CONFIG_KEYS, NO_REPO_PATHS, REPO_UNSETTABLE_CHECK_NAMES, REPO_ARMABLE_GATES;
|
|
1337
1409
|
var init_config_validation = __esm(() => {
|
|
1410
|
+
init_config_merge();
|
|
1338
1411
|
ConfigValidationError = class ConfigValidationError extends Error {
|
|
1339
1412
|
issues;
|
|
1340
1413
|
constructor(message, issues) {
|
|
@@ -1352,6 +1425,18 @@ var init_config_validation = __esm(() => {
|
|
|
1352
1425
|
note: "review, auto-fix and deep-review are contained and pin their own sources"
|
|
1353
1426
|
}
|
|
1354
1427
|
];
|
|
1428
|
+
NO_REPO_PATHS = new Set;
|
|
1429
|
+
REPO_UNSETTABLE_CHECK_NAMES = [
|
|
1430
|
+
"review.autoMerge.independentReview.checkName",
|
|
1431
|
+
"review.autoMerge.independentReview.verdict.checkName"
|
|
1432
|
+
];
|
|
1433
|
+
REPO_ARMABLE_GATES = {
|
|
1434
|
+
"review.autoMerge.independentReview.enabled": "tightening",
|
|
1435
|
+
"review.autoMerge.interactive": "handshake",
|
|
1436
|
+
"review.autoMerge.ciRepair.enabled": "handshake",
|
|
1437
|
+
"review.autoMerge.ciRepair.patch.enabled": "handshake",
|
|
1438
|
+
"review.autoMerge.findingsLoop": "handshake"
|
|
1439
|
+
};
|
|
1355
1440
|
});
|
|
1356
1441
|
|
|
1357
1442
|
// src/paths.ts
|
|
@@ -1498,6 +1583,9 @@ var init_branchRef = __esm(() => {
|
|
|
1498
1583
|
PR_LINK_PATTERN = /PR:\s*(https?:\/\/[^\s)]+)/;
|
|
1499
1584
|
});
|
|
1500
1585
|
|
|
1586
|
+
// ../harmony-shared/dist/cardLifecycle.js
|
|
1587
|
+
var init_cardLifecycle = () => {};
|
|
1588
|
+
|
|
1501
1589
|
// ../harmony-shared/dist/cardLinks.js
|
|
1502
1590
|
var init_cardLinks = () => {};
|
|
1503
1591
|
|
|
@@ -2986,6 +3074,7 @@ function untrustedDataBlock(text, options) {
|
|
|
2986
3074
|
var init_dist = __esm(() => {
|
|
2987
3075
|
init_agentStaleness();
|
|
2988
3076
|
init_branchRef();
|
|
3077
|
+
init_cardLifecycle();
|
|
2989
3078
|
init_cardLinks();
|
|
2990
3079
|
init_cardModelStat();
|
|
2991
3080
|
init_classification();
|
|
@@ -3349,16 +3438,17 @@ var init_types2 = __esm(() => {
|
|
|
3349
3438
|
},
|
|
3350
3439
|
claude: {
|
|
3351
3440
|
model: "claude-opus-5",
|
|
3352
|
-
escalateModel: "claude-fable-5",
|
|
3441
|
+
escalateModel: "claude-fable-5-1",
|
|
3353
3442
|
escalateAfterAttempts: 2,
|
|
3354
3443
|
tiers: {
|
|
3355
3444
|
simple: "claude-sonnet-5",
|
|
3356
3445
|
advanced: "claude-opus-5",
|
|
3357
|
-
research: "claude-fable-5"
|
|
3446
|
+
research: "claude-fable-5-1"
|
|
3358
3447
|
},
|
|
3359
3448
|
scoreBands: DEFAULT_SCORE_BANDS,
|
|
3360
3449
|
sizingModel: "",
|
|
3361
3450
|
reviewModel: "sonnet",
|
|
3451
|
+
deepReviewModel: "sonnet",
|
|
3362
3452
|
maxTurns: 240,
|
|
3363
3453
|
reviewMaxTurns: 60,
|
|
3364
3454
|
additionalArgs: []
|
|
@@ -3473,6 +3563,302 @@ var init_types2 = __esm(() => {
|
|
|
3473
3563
|
NOTICE_IDENTIFIER = `${DAEMON_IDENTIFIER_BASE}-notice`;
|
|
3474
3564
|
});
|
|
3475
3565
|
|
|
3566
|
+
// src/repo-agent-config.ts
|
|
3567
|
+
import { readFileSync } from "node:fs";
|
|
3568
|
+
import { findLocalConfigPath } from "@gethmy/mcp/src/config.js";
|
|
3569
|
+
function isNeverSettable(path) {
|
|
3570
|
+
for (const banned of REPO_NEVER_SETTABLE) {
|
|
3571
|
+
if (path === banned || path.startsWith(`${banned}.`))
|
|
3572
|
+
return true;
|
|
3573
|
+
}
|
|
3574
|
+
return false;
|
|
3575
|
+
}
|
|
3576
|
+
function renderForWarning(value) {
|
|
3577
|
+
const json = JSON.stringify(clampForRender(value)) ?? String(value);
|
|
3578
|
+
const clean = json.replace(/\p{Cf}/gu, "").replace(/[\p{Cc}\u2028\u2029]/gu, " ");
|
|
3579
|
+
if (clean.length <= MAX_RENDERED_LENGTH)
|
|
3580
|
+
return clean;
|
|
3581
|
+
const head = clean.slice(0, MAX_RENDERED_LENGTH).replace(/\\u[0-9a-fA-F]{0,3}$/, "").replace(/\\+$/, "").replace(/[\uD800-\uDBFF]$/, "");
|
|
3582
|
+
const closer = clean.startsWith('"') ? '"' : "";
|
|
3583
|
+
return `${head}…${closer} (truncated from ${clean.length} characters)`;
|
|
3584
|
+
}
|
|
3585
|
+
function clampForRender(value, depth = 0) {
|
|
3586
|
+
if (Array.isArray(value)) {
|
|
3587
|
+
if (depth >= MAX_RENDER_DEPTH)
|
|
3588
|
+
return ELISION;
|
|
3589
|
+
const kept = value.slice(0, MAX_RENDER_ENTRIES).map((item) => clampForRender(item, depth + 1));
|
|
3590
|
+
if (value.length > MAX_RENDER_ENTRIES)
|
|
3591
|
+
kept.push(ELISION);
|
|
3592
|
+
return kept;
|
|
3593
|
+
}
|
|
3594
|
+
if (isPlainObject(value)) {
|
|
3595
|
+
if (depth >= MAX_RENDER_DEPTH)
|
|
3596
|
+
return ELISION;
|
|
3597
|
+
const keys = Object.keys(value);
|
|
3598
|
+
const out = Object.create(null);
|
|
3599
|
+
for (const key of keys.slice(0, MAX_RENDER_ENTRIES)) {
|
|
3600
|
+
out[key] = clampForRender(value[key], depth + 1);
|
|
3601
|
+
}
|
|
3602
|
+
return out;
|
|
3603
|
+
}
|
|
3604
|
+
return value;
|
|
3605
|
+
}
|
|
3606
|
+
function describeParent(drop) {
|
|
3607
|
+
return drop.parent ? ` inside ${renderForWarning(`agent.${drop.parent}`)}` : ` inside "agent"`;
|
|
3608
|
+
}
|
|
3609
|
+
function isOneSegment(key) {
|
|
3610
|
+
return key.length > 0 && !key.includes(".") && !UNPRINTABLE.test(key);
|
|
3611
|
+
}
|
|
3612
|
+
function refuseValue(value, depth = 0) {
|
|
3613
|
+
if (typeof value === "string") {
|
|
3614
|
+
if (UNPRINTABLE.test(value))
|
|
3615
|
+
return `A repo-supplied value may not hold a control or invisible character, and nothing in ${ALLOWED_PREFIX}* is spelled with one.`;
|
|
3616
|
+
if (value.length > MAX_VALUE_LENGTH)
|
|
3617
|
+
return `A repo-supplied value may be at most ${MAX_VALUE_LENGTH} characters, and this one is ${value.length}.`;
|
|
3618
|
+
return null;
|
|
3619
|
+
}
|
|
3620
|
+
if (depth >= MAX_VALUE_DEPTH)
|
|
3621
|
+
return `A repo-supplied value may not nest more than ${MAX_VALUE_DEPTH} levels deep.`;
|
|
3622
|
+
if (Array.isArray(value)) {
|
|
3623
|
+
for (const item of value) {
|
|
3624
|
+
const why = refuseValue(item, depth + 1);
|
|
3625
|
+
if (why !== null)
|
|
3626
|
+
return why;
|
|
3627
|
+
}
|
|
3628
|
+
return null;
|
|
3629
|
+
}
|
|
3630
|
+
if (isPlainObject(value)) {
|
|
3631
|
+
for (const [key, item] of Object.entries(value)) {
|
|
3632
|
+
const why = refuseValue(key, depth + 1) ?? refuseValue(item, depth + 1);
|
|
3633
|
+
if (why !== null)
|
|
3634
|
+
return why;
|
|
3635
|
+
}
|
|
3636
|
+
return null;
|
|
3637
|
+
}
|
|
3638
|
+
return null;
|
|
3639
|
+
}
|
|
3640
|
+
function flatten(value, prefix, out, dropped, depth = 0) {
|
|
3641
|
+
if (!isPlainObject(value)) {
|
|
3642
|
+
out.set(prefix, value);
|
|
3643
|
+
return;
|
|
3644
|
+
}
|
|
3645
|
+
if (depth >= MAX_BLOCK_DEPTH) {
|
|
3646
|
+
dropped.push({
|
|
3647
|
+
path: prefix,
|
|
3648
|
+
key: lastSegment(prefix),
|
|
3649
|
+
parent: parentPath(prefix),
|
|
3650
|
+
why: "depth"
|
|
3651
|
+
});
|
|
3652
|
+
return;
|
|
3653
|
+
}
|
|
3654
|
+
const keys = Object.keys(value);
|
|
3655
|
+
if (keys.length === 0)
|
|
3656
|
+
return;
|
|
3657
|
+
for (const key of keys) {
|
|
3658
|
+
const here = prefix ? `${prefix}.${key}` : key;
|
|
3659
|
+
if (FORBIDDEN_KEYS.has(key)) {
|
|
3660
|
+
dropped.push({ path: here, key, parent: prefix, why: "reserved" });
|
|
3661
|
+
continue;
|
|
3662
|
+
}
|
|
3663
|
+
if (!isOneSegment(key)) {
|
|
3664
|
+
dropped.push({
|
|
3665
|
+
path: here,
|
|
3666
|
+
key,
|
|
3667
|
+
parent: prefix,
|
|
3668
|
+
why: UNPRINTABLE.test(key) ? "unprintable" : "segment"
|
|
3669
|
+
});
|
|
3670
|
+
continue;
|
|
3671
|
+
}
|
|
3672
|
+
flatten(value[key], here, out, dropped, depth + 1);
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
function lastSegment(path) {
|
|
3676
|
+
const at = path.lastIndexOf(".");
|
|
3677
|
+
return at === -1 ? path : path.slice(at + 1);
|
|
3678
|
+
}
|
|
3679
|
+
function parentPath(path) {
|
|
3680
|
+
const at = path.lastIndexOf(".");
|
|
3681
|
+
return at === -1 ? "" : path.slice(0, at);
|
|
3682
|
+
}
|
|
3683
|
+
function setPath(target, path, value) {
|
|
3684
|
+
const parts = path.split(".");
|
|
3685
|
+
if (parts.some((part) => FORBIDDEN_KEYS.has(part) || !isOneSegment(part)))
|
|
3686
|
+
return false;
|
|
3687
|
+
let node = target;
|
|
3688
|
+
for (const part of parts.slice(0, -1)) {
|
|
3689
|
+
const existing = own(node, part);
|
|
3690
|
+
if (!isPlainObject(existing))
|
|
3691
|
+
node[part] = {};
|
|
3692
|
+
node = node[part];
|
|
3693
|
+
}
|
|
3694
|
+
node[parts[parts.length - 1]] = value;
|
|
3695
|
+
return true;
|
|
3696
|
+
}
|
|
3697
|
+
function describeDrop(pinPath, drop) {
|
|
3698
|
+
switch (drop.why) {
|
|
3699
|
+
case "reserved":
|
|
3700
|
+
return `${pinPath}: ${renderForWarning(`agent.${drop.path}`)} uses a reserved JavaScript key name — ignoring it. Nothing in ${ALLOWED_PREFIX}* is spelled that way.`;
|
|
3701
|
+
case "unprintable":
|
|
3702
|
+
return `${pinPath}: ${renderForWarning(drop.key)} is not a setting name${describeParent(drop)} — ignoring it. A name may not hold a control or invisible character, and nothing in ${ALLOWED_PREFIX}* is spelled with one.`;
|
|
3703
|
+
case "depth":
|
|
3704
|
+
return `${pinPath}: ${renderForWarning(`agent.${drop.path}`)} nests more than ${MAX_BLOCK_DEPTH} blocks deep — ignoring it and everything under it. The deepest setting anything reads is ${ALLOWED_PREFIX}ciRepair.patch.enabled, which is 5 levels.`;
|
|
3705
|
+
default:
|
|
3706
|
+
return `${pinPath}: ${renderForWarning(drop.key)} is ONE key${describeParent(drop)}, not the nesting it looks like — ignoring it. Write a setting as real nesting: {"review": {"autoMerge": {"enabled": false}}}. A key must be a name, so it may not be empty and may not contain a ".".`;
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
function describeValue(value) {
|
|
3710
|
+
if (value === null)
|
|
3711
|
+
return "null";
|
|
3712
|
+
if (Array.isArray(value))
|
|
3713
|
+
return "a list";
|
|
3714
|
+
if (isPlainObject(value))
|
|
3715
|
+
return "a block of settings";
|
|
3716
|
+
return `a ${typeof value}`;
|
|
3717
|
+
}
|
|
3718
|
+
function mismatchedPath(message) {
|
|
3719
|
+
return message.slice(0, message.indexOf(" is "));
|
|
3720
|
+
}
|
|
3721
|
+
function readRepoAgentConfig(args) {
|
|
3722
|
+
const nothing = {
|
|
3723
|
+
overrides: {},
|
|
3724
|
+
warnings: [],
|
|
3725
|
+
suppliedPaths: new Set
|
|
3726
|
+
};
|
|
3727
|
+
const pinPath = findLocalConfigPath(args.repoRoot);
|
|
3728
|
+
if (pinPath === null)
|
|
3729
|
+
return nothing;
|
|
3730
|
+
let parsed;
|
|
3731
|
+
try {
|
|
3732
|
+
parsed = JSON.parse(readFileSync(pinPath, "utf-8"));
|
|
3733
|
+
} catch {
|
|
3734
|
+
return nothing;
|
|
3735
|
+
}
|
|
3736
|
+
if (!isPlainObject(parsed))
|
|
3737
|
+
return nothing;
|
|
3738
|
+
const warnings = [];
|
|
3739
|
+
for (const key of Object.keys(parsed)) {
|
|
3740
|
+
if (KNOWN_PIN_KEYS.includes(key))
|
|
3741
|
+
continue;
|
|
3742
|
+
warnings.push(`${pinPath}: ${renderForWarning(key)} is not a key anything reads — ignoring it. Known keys: ${KNOWN_PIN_KEYS.join(", ")}`);
|
|
3743
|
+
}
|
|
3744
|
+
const block = own(parsed, "agent");
|
|
3745
|
+
if (block !== undefined && !isPlainObject(block)) {
|
|
3746
|
+
warnings.push(`${pinPath}: "agent" is ${describeValue(block)} — it must be a block of ${ALLOWED_PREFIX}* settings. Ignoring it.`);
|
|
3747
|
+
}
|
|
3748
|
+
if (!isPlainObject(block))
|
|
3749
|
+
return { overrides: {}, warnings, suppliedPaths: new Set };
|
|
3750
|
+
const leaves = new Map;
|
|
3751
|
+
const dropped = [];
|
|
3752
|
+
flatten(block, "", leaves, dropped);
|
|
3753
|
+
for (const drop of dropped) {
|
|
3754
|
+
warnings.push(describeDrop(pinPath, drop));
|
|
3755
|
+
}
|
|
3756
|
+
const inScope = [];
|
|
3757
|
+
const candidate = {};
|
|
3758
|
+
for (const [path, value] of leaves) {
|
|
3759
|
+
if (path === ALLOWED_BLOCK) {
|
|
3760
|
+
warnings.push(`${pinPath}: ${renderForWarning(`agent.${path}`)} is ${describeValue(value)} — it must be a block of ${ALLOWED_PREFIX}* settings, so there is no setting here to read. Ignoring it.`);
|
|
3761
|
+
continue;
|
|
3762
|
+
}
|
|
3763
|
+
if (!path.startsWith(ALLOWED_PREFIX)) {
|
|
3764
|
+
warnings.push(`${pinPath}: ${renderForWarning(`agent.${path}`)} is global config, not repo config — ignoring it. A repository may set ${ALLOWED_PREFIX}* and nothing else.`);
|
|
3765
|
+
continue;
|
|
3766
|
+
}
|
|
3767
|
+
if (isNeverSettable(path)) {
|
|
3768
|
+
warnings.push(`${pinPath}: ${renderForWarning(`agent.${path}`)} is an EXECUTION parameter, not merge policy — ignoring it. It decides what the daemon RUNS on this machine (the container image a CI repair is verified in, and how long it may run), so it stays with identity, credentials, "pool" and "claude.*" in ${args.globalConfigPath}. "allowRepoOverrides" does not reach it.`);
|
|
3769
|
+
continue;
|
|
3770
|
+
}
|
|
3771
|
+
const badValue = refuseValue(value);
|
|
3772
|
+
if (badValue !== null) {
|
|
3773
|
+
warnings.push(`${pinPath}: ${renderForWarning(`agent.${path}`)} = ${renderForWarning(value)} is not a value a repository may set — ignoring it. ${badValue}`);
|
|
3774
|
+
continue;
|
|
3775
|
+
}
|
|
3776
|
+
if (!setPath(candidate, path, value)) {
|
|
3777
|
+
warnings.push(`${pinPath}: ${renderForWarning(`agent.${path}`)} contains a reserved JavaScript key name — ignoring it. Nothing in ${ALLOWED_PREFIX}* is spelled that way.`);
|
|
3778
|
+
continue;
|
|
3779
|
+
}
|
|
3780
|
+
inScope.push([path, value]);
|
|
3781
|
+
}
|
|
3782
|
+
const malformed = [];
|
|
3783
|
+
for (const message of findConfigTypeMismatches(DEFAULT_AGENT_CONFIG, candidate)) {
|
|
3784
|
+
warnings.push(`${pinPath}: ${message}`);
|
|
3785
|
+
malformed.push(mismatchedPath(message).replace(/^agent\./, ""));
|
|
3786
|
+
}
|
|
3787
|
+
const kept = {};
|
|
3788
|
+
const suppliedPaths = new Set;
|
|
3789
|
+
for (const [path, value] of inScope) {
|
|
3790
|
+
if (malformed.some((bad) => path === bad || path.startsWith(`${bad}.`)))
|
|
3791
|
+
continue;
|
|
3792
|
+
if (!args.allowRepoOverrides && FREE_VALUE.get(path) !== value) {
|
|
3793
|
+
warnings.push(`${pinPath}: ${renderForWarning(`agent.${path}`)} = ${renderForWarning(value)} would LOOSEN this machine's merge policy — ignoring it. To honour it, set "allowRepoOverrides": true at the TOP LEVEL of ${args.globalConfigPath}, beside "apiKey" — inside the "agent" block it is read by nobody.`);
|
|
3794
|
+
continue;
|
|
3795
|
+
}
|
|
3796
|
+
if (!setPath(kept, path, value))
|
|
3797
|
+
continue;
|
|
3798
|
+
suppliedPaths.add(path);
|
|
3799
|
+
const named = NAMED_REVIEW_PATHS.get(path);
|
|
3800
|
+
if (named !== undefined) {
|
|
3801
|
+
warnings.push(`${pinPath}: ${renderForWarning(`agent.${path}`)} = ${renderForWarning(value)} — ${named.effect} Accepted because "allowRepoOverrides": true is set in ${args.globalConfigPath}.${named.extra ? ` ${named.extra}` : ""}`);
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
return {
|
|
3805
|
+
overrides: kept,
|
|
3806
|
+
warnings,
|
|
3807
|
+
suppliedPaths
|
|
3808
|
+
};
|
|
3809
|
+
}
|
|
3810
|
+
var KNOWN_PIN_KEYS, ALLOWED_PREFIX = "review.autoMerge.", ALLOWED_BLOCK, FREE_VALUE, REPO_NEVER_SETTABLE, MAX_RENDERED_LENGTH = 120, ELISION = "…", MAX_RENDER_DEPTH = 4, MAX_RENDER_ENTRIES = 20, NAMED_REVIEW_PATHS, UNPRINTABLE, MAX_VALUE_LENGTH = 200, MAX_VALUE_DEPTH = 8, MAX_BLOCK_DEPTH = 8;
|
|
3811
|
+
var init_repo_agent_config = __esm(() => {
|
|
3812
|
+
init_config_merge();
|
|
3813
|
+
init_types2();
|
|
3814
|
+
KNOWN_PIN_KEYS = [
|
|
3815
|
+
"workspaceId",
|
|
3816
|
+
"projectId",
|
|
3817
|
+
"commands",
|
|
3818
|
+
"agent"
|
|
3819
|
+
];
|
|
3820
|
+
ALLOWED_BLOCK = ALLOWED_PREFIX.slice(0, -1);
|
|
3821
|
+
FREE_VALUE = new Map([
|
|
3822
|
+
["review.autoMerge.enabled", false],
|
|
3823
|
+
["review.autoMerge.interactive", false],
|
|
3824
|
+
["review.autoMerge.findingsLoop", false],
|
|
3825
|
+
["review.autoMerge.ciRepair.enabled", false],
|
|
3826
|
+
["review.autoMerge.ciRepair.patch.enabled", false],
|
|
3827
|
+
["review.autoMerge.requireGreenCi", true],
|
|
3828
|
+
["review.autoMerge.reReviewOnBranchChange", true],
|
|
3829
|
+
["review.autoMerge.independentReview.enabled", true],
|
|
3830
|
+
["review.autoMerge.independentReview.verdict.enabled", true]
|
|
3831
|
+
]);
|
|
3832
|
+
REPO_NEVER_SETTABLE = new Set([
|
|
3833
|
+
"review.autoMerge.ciRepair.patch.sandboxImage",
|
|
3834
|
+
"review.autoMerge.ciRepair.patch.sandboxTimeoutMs"
|
|
3835
|
+
]);
|
|
3836
|
+
NAMED_REVIEW_PATHS = new Map([
|
|
3837
|
+
[
|
|
3838
|
+
"review.autoMerge.independentReview.checkName",
|
|
3839
|
+
{
|
|
3840
|
+
effect: "this repository now decides which CI check stands for the independent review, and a name that resolves to an unrelated green check satisfies the merge gate with nothing having read the diff.",
|
|
3841
|
+
extra: `While ${ALLOWED_PREFIX}interactive or ${ALLOWED_PREFIX}enabled is on, a repo-supplied check name is refused at startup.`
|
|
3842
|
+
}
|
|
3843
|
+
],
|
|
3844
|
+
[
|
|
3845
|
+
"review.autoMerge.independentReview.verdict.checkName",
|
|
3846
|
+
{
|
|
3847
|
+
effect: "this repository now decides which CI check carries the independent review's VERDICT, and a name that resolves to an unrelated green check satisfies the merge gate with nothing having approved the diff.",
|
|
3848
|
+
extra: `While ${ALLOWED_PREFIX}interactive or ${ALLOWED_PREFIX}enabled is on, a repo-supplied check name is refused at startup.`
|
|
3849
|
+
}
|
|
3850
|
+
],
|
|
3851
|
+
[
|
|
3852
|
+
"review.autoMerge.independentReview.label",
|
|
3853
|
+
{
|
|
3854
|
+
effect: "this repository now decides which PR label asks CI for the independent review. It names no check and cannot stand in for one: a label CI does not act on means the review never arrives, so the merge gate HOLDS rather than opening.",
|
|
3855
|
+
extra: "Nothing refuses it at startup, for that reason — unlike a repo-supplied check name."
|
|
3856
|
+
}
|
|
3857
|
+
]
|
|
3858
|
+
]);
|
|
3859
|
+
UNPRINTABLE = /[\p{Cc}\p{Cf}]/u;
|
|
3860
|
+
});
|
|
3861
|
+
|
|
3476
3862
|
// src/config.ts
|
|
3477
3863
|
var exports_config = {};
|
|
3478
3864
|
__export(exports_config, {
|
|
@@ -3484,11 +3870,11 @@ __export(exports_config, {
|
|
|
3484
3870
|
createApiClient: () => createApiClient
|
|
3485
3871
|
});
|
|
3486
3872
|
import { execSync } from "node:child_process";
|
|
3487
|
-
import { readFileSync } from "node:fs";
|
|
3873
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
3488
3874
|
import { log as log5 } from "@gethmy/harness";
|
|
3489
3875
|
import { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
|
|
3490
3876
|
import {
|
|
3491
|
-
findLocalConfigPath,
|
|
3877
|
+
findLocalConfigPath as findLocalConfigPath2,
|
|
3492
3878
|
getActiveProjectId,
|
|
3493
3879
|
getActiveWorkspaceId,
|
|
3494
3880
|
getApiKey,
|
|
@@ -3500,9 +3886,9 @@ import {
|
|
|
3500
3886
|
import { refreshOAuthToken } from "@gethmy/mcp/src/oauth-refresh.js";
|
|
3501
3887
|
function readConfigFile() {
|
|
3502
3888
|
try {
|
|
3503
|
-
return
|
|
3889
|
+
return readFileSync2(configPath(), "utf-8");
|
|
3504
3890
|
} catch {
|
|
3505
|
-
const raw =
|
|
3891
|
+
const raw = readFileSync2(legacyConfigPath(), "utf-8");
|
|
3506
3892
|
if (!warnedLegacyConfigDir) {
|
|
3507
3893
|
warnedLegacyConfigDir = true;
|
|
3508
3894
|
log5.warn(TAG5, `Read the pre-#1082 config at ${legacyConfigPath()}. The current ` + `location is ${configPath()} — the daemon migrates it on its next ` + `\`run\`, and this fallback is temporary.`);
|
|
@@ -3556,22 +3942,37 @@ function loadDaemonConfig() {
|
|
|
3556
3942
|
let agentName = "Harmony Agent";
|
|
3557
3943
|
let agentIdentifier2 = "harmony-daemon";
|
|
3558
3944
|
let agentColor = "#57b8a5";
|
|
3945
|
+
let allowRepoOverrides = false;
|
|
3559
3946
|
const configWarnings = [...contextWarnings];
|
|
3560
3947
|
try {
|
|
3561
3948
|
const raw = readConfigFile();
|
|
3562
3949
|
const parsed = JSON.parse(raw);
|
|
3563
3950
|
configWarnings.push(...findRemovedConfigKeys(parsed));
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
if (typeof
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3951
|
+
const block = own(parsed, "agent");
|
|
3952
|
+
if (block) {
|
|
3953
|
+
agentOverrides = block;
|
|
3954
|
+
}
|
|
3955
|
+
const rawName = own(parsed, "agentName");
|
|
3956
|
+
if (typeof rawName === "string" && rawName.trim())
|
|
3957
|
+
agentName = rawName.trim();
|
|
3958
|
+
const rawIdentifier = own(parsed, "agentIdentifier");
|
|
3959
|
+
if (typeof rawIdentifier === "string" && rawIdentifier.trim())
|
|
3960
|
+
agentIdentifier2 = rawIdentifier.trim();
|
|
3961
|
+
const rawColor = own(parsed, "agentColor");
|
|
3962
|
+
if (typeof rawColor === "string" && rawColor.trim())
|
|
3963
|
+
agentColor = rawColor.trim();
|
|
3964
|
+
if (own(parsed, "allowRepoOverrides") === true)
|
|
3965
|
+
allowRepoOverrides = true;
|
|
3573
3966
|
} catch {}
|
|
3574
|
-
const
|
|
3967
|
+
const repo = readRepoAgentConfig({
|
|
3968
|
+
repoRoot,
|
|
3969
|
+
globalConfigPath: contextSource.globalPath,
|
|
3970
|
+
allowRepoOverrides
|
|
3971
|
+
});
|
|
3972
|
+
configWarnings.push(...repo.warnings);
|
|
3973
|
+
const operatorAgent = mergeAgentConfig(DEFAULT_AGENT_CONFIG, agentOverrides);
|
|
3974
|
+
const agent = mergeAgentConfig(operatorAgent, repo.overrides);
|
|
3975
|
+
const repoArmedGates = repoArmedGatesOf(repo.suppliedPaths, operatorAgent, agent);
|
|
3575
3976
|
configWarnings.push(...findConfigTypeMismatches(DEFAULT_AGENT_CONFIG, agentOverrides));
|
|
3576
3977
|
if (agent.runner !== "cli" && agent.runner !== "sdk") {
|
|
3577
3978
|
agent.runner = DEFAULT_AGENT_CONFIG.runner;
|
|
@@ -3588,13 +3989,16 @@ function loadDaemonConfig() {
|
|
|
3588
3989
|
agentColor,
|
|
3589
3990
|
agent,
|
|
3590
3991
|
agentOverrides,
|
|
3992
|
+
repoAgentOverrides: repo.overrides,
|
|
3993
|
+
repoAgentPaths: repo.suppliedPaths,
|
|
3994
|
+
repoArmedGates,
|
|
3591
3995
|
contextSource,
|
|
3592
3996
|
configWarnings
|
|
3593
3997
|
};
|
|
3594
3998
|
}
|
|
3595
3999
|
function resolveContextSource(repoRoot) {
|
|
3596
4000
|
const globalPath = getConfigPath();
|
|
3597
|
-
const localPath =
|
|
4001
|
+
const localPath = findLocalConfigPath2(repoRoot);
|
|
3598
4002
|
if (localPath === null) {
|
|
3599
4003
|
return {
|
|
3600
4004
|
source: {
|
|
@@ -3648,6 +4052,7 @@ var init_config = __esm(() => {
|
|
|
3648
4052
|
init_config_merge();
|
|
3649
4053
|
init_config_validation();
|
|
3650
4054
|
init_paths();
|
|
4055
|
+
init_repo_agent_config();
|
|
3651
4056
|
init_types2();
|
|
3652
4057
|
});
|
|
3653
4058
|
|
|
@@ -4157,7 +4562,7 @@ import {
|
|
|
4157
4562
|
mkdirSync as mkdirSync2,
|
|
4158
4563
|
mkdtempSync,
|
|
4159
4564
|
readdirSync as readdirSync2,
|
|
4160
|
-
readFileSync as
|
|
4565
|
+
readFileSync as readFileSync3,
|
|
4161
4566
|
rmSync,
|
|
4162
4567
|
statSync as statSync2,
|
|
4163
4568
|
unlinkSync,
|
|
@@ -4234,7 +4639,7 @@ function sameBytes(a, b) {
|
|
|
4234
4639
|
return false;
|
|
4235
4640
|
if (sa.size > MAX_FILE_BYTES)
|
|
4236
4641
|
return false;
|
|
4237
|
-
return
|
|
4642
|
+
return readFileSync3(a).equals(readFileSync3(b));
|
|
4238
4643
|
} catch {
|
|
4239
4644
|
return false;
|
|
4240
4645
|
}
|
|
@@ -4258,7 +4663,7 @@ function applyChanges(args) {
|
|
|
4258
4663
|
for (const path of [...changes.modified, ...changes.added]) {
|
|
4259
4664
|
const from = join3(scratch, path);
|
|
4260
4665
|
const to = safeDestination(clean, path);
|
|
4261
|
-
const bytes =
|
|
4666
|
+
const bytes = readFileSync3(from);
|
|
4262
4667
|
if (bytes.byteLength > MAX_FILE_BYTES) {
|
|
4263
4668
|
throw new Error(`the repair wrote ${path} at ${bytes.byteLength} bytes, over the ${MAX_FILE_BYTES} cap`);
|
|
4264
4669
|
}
|
|
@@ -4315,7 +4720,8 @@ import {
|
|
|
4315
4720
|
isPrUrlOnOrigin,
|
|
4316
4721
|
log as log9,
|
|
4317
4722
|
removeWorktreeHoldingBranch,
|
|
4318
|
-
resolvePrHeadBranch
|
|
4723
|
+
resolvePrHeadBranch,
|
|
4724
|
+
shouldInstall
|
|
4319
4725
|
} from "@gethmy/harness";
|
|
4320
4726
|
function gitErrorDetail(err) {
|
|
4321
4727
|
const e = err;
|
|
@@ -4372,6 +4778,8 @@ function checkoutExistingBranch(basePath, branchName, opts = {}) {
|
|
|
4372
4778
|
} catch (err) {
|
|
4373
4779
|
throw new Error(`Failed to create review worktree for ${branchName}: ${gitErrorDetail(err)}`);
|
|
4374
4780
|
}
|
|
4781
|
+
if (!shouldInstall(worktreeDir, opts.commands))
|
|
4782
|
+
return worktreeDir;
|
|
4375
4783
|
log9.info(TAG9, "Installing dependencies in review worktree...");
|
|
4376
4784
|
try {
|
|
4377
4785
|
execSync2(installCommand(opts.ignoreScripts !== false), {
|
|
@@ -4458,7 +4866,7 @@ var init_review_worktree = __esm(() => {
|
|
|
4458
4866
|
// src/ci-patch.ts
|
|
4459
4867
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
4460
4868
|
import { randomUUID } from "node:crypto";
|
|
4461
|
-
import { readFileSync as
|
|
4869
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
4462
4870
|
import { join as join4 } from "node:path";
|
|
4463
4871
|
import {
|
|
4464
4872
|
buildCommand,
|
|
@@ -4468,6 +4876,7 @@ import {
|
|
|
4468
4876
|
GIT_NO_HOOKS as GIT_NO_HOOKS4,
|
|
4469
4877
|
HARMONY_CREDENTIAL_KEYS,
|
|
4470
4878
|
log as log10,
|
|
4879
|
+
REPO_CONFIG_FILENAMES,
|
|
4471
4880
|
runInSandbox,
|
|
4472
4881
|
SdkAgentRunner,
|
|
4473
4882
|
sandboxAvailable,
|
|
@@ -4559,7 +4968,10 @@ async function verifyInSandbox(worktree, config) {
|
|
|
4559
4968
|
return null;
|
|
4560
4969
|
}
|
|
4561
4970
|
let ran = null;
|
|
4562
|
-
for (const command of [
|
|
4971
|
+
for (const command of [
|
|
4972
|
+
buildCommand(worktree, config.commands),
|
|
4973
|
+
testCommand(worktree, config.commands)
|
|
4974
|
+
]) {
|
|
4563
4975
|
if (!command)
|
|
4564
4976
|
continue;
|
|
4565
4977
|
ran = command;
|
|
@@ -4593,7 +5005,7 @@ function commitRepair(worktree, card) {
|
|
|
4593
5005
|
}
|
|
4594
5006
|
function readGitPointer(worktree) {
|
|
4595
5007
|
try {
|
|
4596
|
-
return
|
|
5008
|
+
return readFileSync4(join4(worktree, ".git"), "utf-8");
|
|
4597
5009
|
} catch {
|
|
4598
5010
|
return null;
|
|
4599
5011
|
}
|
|
@@ -4620,7 +5032,8 @@ async function repairBranch(deps) {
|
|
|
4620
5032
|
let trees = null;
|
|
4621
5033
|
try {
|
|
4622
5034
|
worktree = checkoutExistingBranch(config.worktree.basePath, branch, {
|
|
4623
|
-
ignoreScripts: true
|
|
5035
|
+
ignoreScripts: true,
|
|
5036
|
+
commands: config.commands
|
|
4624
5037
|
});
|
|
4625
5038
|
const baseSha = remoteSha(branch, worktree);
|
|
4626
5039
|
if (!baseSha) {
|
|
@@ -4762,7 +5175,8 @@ var init_ci_patch = __esm(() => {
|
|
|
4762
5175
|
/(^|\/)\.yarnrc\.ya?ml$/i,
|
|
4763
5176
|
/(^|\/)\.tool-versions$/i,
|
|
4764
5177
|
/(^|\/)\.gitattributes$/i,
|
|
4765
|
-
/(^|\/)\.gitconfig$/i
|
|
5178
|
+
/(^|\/)\.gitconfig$/i,
|
|
5179
|
+
...REPO_CONFIG_FILENAMES.map((name) => new RegExp(`(^|/)${name.replace(/\./g, "\\.")}$`, "i"))
|
|
4766
5180
|
];
|
|
4767
5181
|
});
|
|
4768
5182
|
|
|
@@ -6478,7 +6892,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
6478
6892
|
revertWarnings: []
|
|
6479
6893
|
};
|
|
6480
6894
|
if (config.verification.enabled && config.verification.lint) {
|
|
6481
|
-
await runFormatFix(worktreePath, config.verification.timeout, workerId, verificationSandbox(config));
|
|
6895
|
+
await runFormatFix(worktreePath, config.verification.timeout, workerId, verificationSandbox(config), config.commands);
|
|
6482
6896
|
}
|
|
6483
6897
|
commitUncommittedChanges(worktreePath, card);
|
|
6484
6898
|
const hasCommits = checkHasCommits(worktreePath, config.worktree.baseBranch, runBaselineSha);
|
|
@@ -7447,7 +7861,7 @@ var init_prompt = __esm(() => {
|
|
|
7447
7861
|
});
|
|
7448
7862
|
|
|
7449
7863
|
// src/review-completion.ts
|
|
7450
|
-
import { readFileSync as
|
|
7864
|
+
import { readFileSync as readFileSync5, statSync as statSync3 } from "node:fs";
|
|
7451
7865
|
import {
|
|
7452
7866
|
cleanupWorktree as cleanupWorktree3,
|
|
7453
7867
|
createPullRequest as createPullRequest2,
|
|
@@ -7529,7 +7943,7 @@ function tailRunLog(path, bytes = RUN_LOG_TAIL_BYTES) {
|
|
|
7529
7943
|
if (size === 0)
|
|
7530
7944
|
return null;
|
|
7531
7945
|
const start = Math.max(0, size - bytes);
|
|
7532
|
-
const buf =
|
|
7946
|
+
const buf = readFileSync5(path);
|
|
7533
7947
|
return buf.subarray(start).toString("utf-8");
|
|
7534
7948
|
} catch {
|
|
7535
7949
|
return null;
|
|
@@ -8253,7 +8667,7 @@ __export(exports_state_store, {
|
|
|
8253
8667
|
import {
|
|
8254
8668
|
existsSync as existsSync3,
|
|
8255
8669
|
mkdirSync as mkdirSync4,
|
|
8256
|
-
readFileSync as
|
|
8670
|
+
readFileSync as readFileSync6,
|
|
8257
8671
|
renameSync as renameSync2,
|
|
8258
8672
|
writeFileSync as writeFileSync2
|
|
8259
8673
|
} from "node:fs";
|
|
@@ -8390,7 +8804,7 @@ class StateStore {
|
|
|
8390
8804
|
if (!existsSync3(this.path))
|
|
8391
8805
|
return emptyState();
|
|
8392
8806
|
try {
|
|
8393
|
-
const raw =
|
|
8807
|
+
const raw = readFileSync6(this.path, "utf-8");
|
|
8394
8808
|
const parsed = JSON.parse(raw);
|
|
8395
8809
|
if (parsed?.version !== SCHEMA_VERSION) {
|
|
8396
8810
|
log26.warn(TAG23, `state file has version ${parsed?.version}, expected ${SCHEMA_VERSION} — migrating (preserving card budget/attempts, dropping in-flight runs)`);
|
|
@@ -9059,6 +9473,7 @@ import {
|
|
|
9059
9473
|
buildGateCollectorRegistry,
|
|
9060
9474
|
cleanupWorktree as cleanupWorktree4,
|
|
9061
9475
|
collectGateEvidence,
|
|
9476
|
+
DEV_SERVER_READY_TIMEOUT_MS,
|
|
9062
9477
|
DevServerReadinessError,
|
|
9063
9478
|
devServerLaunch,
|
|
9064
9479
|
devServerSkipReason,
|
|
@@ -9322,7 +9737,7 @@ class ReviewWorker {
|
|
|
9322
9737
|
}
|
|
9323
9738
|
this.sessionId = reviewSession && typeof reviewSession === "object" && "id" in reviewSession ? reviewSession.id ?? null : null;
|
|
9324
9739
|
const labelPromise = addLabelByName(this.client, card, "agent", "#8b5cf6");
|
|
9325
|
-
this.worktreePath = checkoutExistingBranch(this.config.worktree.basePath, this.branchName);
|
|
9740
|
+
this.worktreePath = checkoutExistingBranch(this.config.worktree.basePath, this.branchName, { commands: this.config.commands });
|
|
9326
9741
|
await labelPromise;
|
|
9327
9742
|
}
|
|
9328
9743
|
if (this.aborted)
|
|
@@ -9845,14 +10260,15 @@ No verdict was produced, so this card has not been reviewed. The run's log is at
|
|
|
9845
10260
|
}
|
|
9846
10261
|
}
|
|
9847
10262
|
async startDevServer(card, cwd, port) {
|
|
9848
|
-
const declined = devServerSkipReason(cwd);
|
|
10263
|
+
const declined = devServerSkipReason(cwd, this.config.commands);
|
|
9849
10264
|
if (declined)
|
|
9850
10265
|
return { previewUrl: null, skipReason: declined };
|
|
9851
10266
|
log29.info(this.tag, `Starting dev server on port ${port}...`);
|
|
9852
10267
|
const launch = devServerLaunch({
|
|
9853
10268
|
worktreePath: cwd,
|
|
9854
10269
|
port,
|
|
9855
|
-
sandbox: verificationSandbox2(this.config)
|
|
10270
|
+
sandbox: verificationSandbox2(this.config),
|
|
10271
|
+
operator: this.config.commands
|
|
9856
10272
|
});
|
|
9857
10273
|
this.devServerContainer = launch.containerName ?? null;
|
|
9858
10274
|
if (launch.containerName) {
|
|
@@ -9878,13 +10294,14 @@ No verdict was produced, so this card has not been reviewed. The run's log is at
|
|
|
9878
10294
|
if (devServerSpawnError) {
|
|
9879
10295
|
throw new DevServerReadinessError(`dev server failed to start (${launch.cmd}): ${devServerSpawnError.message}`);
|
|
9880
10296
|
}
|
|
9881
|
-
await waitForDevServer(this.devServerProcess,
|
|
10297
|
+
await waitForDevServer(this.devServerProcess, DEV_SERVER_READY_TIMEOUT_MS);
|
|
9882
10298
|
await probeDevServer(port);
|
|
9883
10299
|
} catch (err) {
|
|
9884
10300
|
this.killDevServer();
|
|
10301
|
+
const failure = sanitizeCiText3(err instanceof Error ? err.message : String(err), REVIEW_FAILURE_REASON_MAX);
|
|
9885
10302
|
return {
|
|
9886
10303
|
previewUrl: null,
|
|
9887
|
-
skipReason:
|
|
10304
|
+
skipReason: launch.origin ? `${launch.origin}, and ${failure}` : failure
|
|
9888
10305
|
};
|
|
9889
10306
|
}
|
|
9890
10307
|
log29.info(this.tag, `Dev server ready on port ${port}`);
|
|
@@ -11257,7 +11674,7 @@ function workspaceConfigToPartial(ws) {
|
|
|
11257
11674
|
}
|
|
11258
11675
|
function resolveEffectiveAgentConfig(cfg, ws) {
|
|
11259
11676
|
const withWorkspace = ws ? mergeAgentConfig(DEFAULT_AGENT_CONFIG, workspaceConfigToPartial(ws)) : DEFAULT_AGENT_CONFIG;
|
|
11260
|
-
return mergeAgentConfig(withWorkspace, cfg.agentOverrides);
|
|
11677
|
+
return mergeAgentConfig(mergeAgentConfig(withWorkspace, cfg.agentOverrides), cfg.repoAgentOverrides);
|
|
11261
11678
|
}
|
|
11262
11679
|
var init_workspace_model_config = __esm(() => {
|
|
11263
11680
|
init_config_merge();
|
|
@@ -11750,7 +12167,8 @@ class Worker {
|
|
|
11750
12167
|
}
|
|
11751
12168
|
this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName, {
|
|
11752
12169
|
continueExisting: target.continueExisting,
|
|
11753
|
-
branchExistsOnOrigin: target.existsOnOrigin
|
|
12170
|
+
branchExistsOnOrigin: target.existsOnOrigin,
|
|
12171
|
+
commands: this.config.commands
|
|
11754
12172
|
});
|
|
11755
12173
|
this.runBaselineSha = readWorktreeHead(this.worktreePath);
|
|
11756
12174
|
}
|
|
@@ -14424,7 +14842,7 @@ __export(exports_port_registry, {
|
|
|
14424
14842
|
import {
|
|
14425
14843
|
existsSync as existsSync4,
|
|
14426
14844
|
mkdirSync as mkdirSync5,
|
|
14427
|
-
readFileSync as
|
|
14845
|
+
readFileSync as readFileSync7,
|
|
14428
14846
|
renameSync as renameSync3,
|
|
14429
14847
|
writeFileSync as writeFileSync4
|
|
14430
14848
|
} from "node:fs";
|
|
@@ -14437,7 +14855,7 @@ function load(path) {
|
|
|
14437
14855
|
if (!existsSync4(path))
|
|
14438
14856
|
return {};
|
|
14439
14857
|
try {
|
|
14440
|
-
const raw =
|
|
14858
|
+
const raw = readFileSync7(path, "utf-8");
|
|
14441
14859
|
const parsed = JSON.parse(raw);
|
|
14442
14860
|
if (parsed && typeof parsed === "object")
|
|
14443
14861
|
return parsed;
|
|
@@ -15889,7 +16307,11 @@ function contextSourceLine(config) {
|
|
|
15889
16307
|
if (unreadablePin !== null) {
|
|
15890
16308
|
return `all of it ${globalPath} (global) — ${unreadablePin} is present but UNREADABLE, so its pin was ignored`;
|
|
15891
16309
|
}
|
|
15892
|
-
|
|
16310
|
+
if (scope !== "repo") {
|
|
16311
|
+
return `all of it ${globalPath} (global; no repo pin found)`;
|
|
16312
|
+
}
|
|
16313
|
+
const pinLabel = config.repoAgentPaths.size > 0 ? "repo pin, +auto-merge" : "repo pin";
|
|
16314
|
+
return `project+workspace ${path} (${pinLabel}) · rest ${globalPath}`;
|
|
15893
16315
|
}
|
|
15894
16316
|
function titleRule(title) {
|
|
15895
16317
|
const prefix = "─── ";
|
|
@@ -15930,7 +16352,7 @@ import {
|
|
|
15930
16352
|
copyFileSync,
|
|
15931
16353
|
existsSync as existsSync5,
|
|
15932
16354
|
mkdirSync as mkdirSync6,
|
|
15933
|
-
readFileSync as
|
|
16355
|
+
readFileSync as readFileSync8,
|
|
15934
16356
|
renameSync as renameSync4,
|
|
15935
16357
|
writeFileSync as writeFileSync5
|
|
15936
16358
|
} from "node:fs";
|
|
@@ -15952,7 +16374,7 @@ function atomicWriteJson(path, value) {
|
|
|
15952
16374
|
}
|
|
15953
16375
|
function readTargetFile(path) {
|
|
15954
16376
|
try {
|
|
15955
|
-
const parsed = JSON.parse(
|
|
16377
|
+
const parsed = JSON.parse(readFileSync8(path, "utf-8"));
|
|
15956
16378
|
if (!parsed || typeof parsed !== "object")
|
|
15957
16379
|
return null;
|
|
15958
16380
|
return parsed;
|
|
@@ -16006,7 +16428,7 @@ function migrateLegacyStateFile(opts) {
|
|
|
16006
16428
|
return NO_MIGRATION;
|
|
16007
16429
|
let legacy;
|
|
16008
16430
|
try {
|
|
16009
|
-
const parsed = JSON.parse(
|
|
16431
|
+
const parsed = JSON.parse(readFileSync8(legacyPath, "utf-8"));
|
|
16010
16432
|
if (!parsed || typeof parsed !== "object")
|
|
16011
16433
|
throw new Error("not an object");
|
|
16012
16434
|
legacy = parsed;
|
|
@@ -16535,7 +16957,7 @@ __export(exports_run_stats, {
|
|
|
16535
16957
|
computeRunStats: () => computeRunStats,
|
|
16536
16958
|
buildRunListRows: () => buildRunListRows
|
|
16537
16959
|
});
|
|
16538
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
16960
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync4 } from "node:fs";
|
|
16539
16961
|
import { join as join8 } from "node:path";
|
|
16540
16962
|
function runsDir2(base) {
|
|
16541
16963
|
return runsDir(base);
|
|
@@ -16618,7 +17040,7 @@ function tailText(path, bytes = 4096) {
|
|
|
16618
17040
|
const size = statSync4(path).size;
|
|
16619
17041
|
if (size === 0)
|
|
16620
17042
|
return "";
|
|
16621
|
-
const buf =
|
|
17043
|
+
const buf = readFileSync9(path);
|
|
16622
17044
|
const start = Math.max(0, size - bytes);
|
|
16623
17045
|
let text = buf.subarray(start).toString("utf-8");
|
|
16624
17046
|
if (start > 0) {
|
|
@@ -16640,7 +17062,7 @@ function grepRunLogs(files, pattern, opts = {}) {
|
|
|
16640
17062
|
continue;
|
|
16641
17063
|
let content;
|
|
16642
17064
|
try {
|
|
16643
|
-
content =
|
|
17065
|
+
content = readFileSync9(file.path, "utf-8");
|
|
16644
17066
|
} catch {
|
|
16645
17067
|
continue;
|
|
16646
17068
|
}
|
|
@@ -17240,7 +17662,9 @@ async function main() {
|
|
|
17240
17662
|
throw err;
|
|
17241
17663
|
}
|
|
17242
17664
|
try {
|
|
17243
|
-
validateAutoMergeConfig(config.agent)
|
|
17665
|
+
for (const warning of validateAutoMergeConfig(config.agent, config.repoAgentPaths, config.repoArmedGates)) {
|
|
17666
|
+
banner.warn(warning);
|
|
17667
|
+
}
|
|
17244
17668
|
validateBudgetConfig(config.agent);
|
|
17245
17669
|
validateSweepConfig(config.agent);
|
|
17246
17670
|
validateRankingConfig(config.agent);
|
|
@@ -17618,14 +18042,14 @@ import { realpathSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
|
17618
18042
|
import { dirname as dirname7, resolve as resolve5, sep as sep3 } from "node:path";
|
|
17619
18043
|
import { GIT_NO_HOOKS as GIT_NO_HOOKS11 } from "@gethmy/harness";
|
|
17620
18044
|
import {
|
|
17621
|
-
findLocalConfigPath as
|
|
18045
|
+
findLocalConfigPath as findLocalConfigPath3,
|
|
17622
18046
|
getLocalConfigPath,
|
|
17623
18047
|
loadLocalConfig as loadLocalConfig2
|
|
17624
18048
|
} from "@gethmy/mcp/src/config.js";
|
|
17625
18049
|
function migrateLocalPin(cwd, opts = {}) {
|
|
17626
18050
|
const here = resolve5(cwd ?? process.cwd());
|
|
17627
18051
|
const boundary = opts.repoRoot ?? gitTopLevel(here) ?? here;
|
|
17628
|
-
const found =
|
|
18052
|
+
const found = findLocalConfigPath3(boundary);
|
|
17629
18053
|
if (found === null) {
|
|
17630
18054
|
return { outcome: "no-pin", from: null, to: null };
|
|
17631
18055
|
}
|
|
@@ -17700,11 +18124,23 @@ function realOrResolved(path) {
|
|
|
17700
18124
|
}
|
|
17701
18125
|
var init_local_pin_migration = () => {};
|
|
17702
18126
|
|
|
18127
|
+
// src/head-anchor.ts
|
|
18128
|
+
import { log as log52 } from "@gethmy/harness";
|
|
18129
|
+
function sealProvenHead(sha, provenance) {
|
|
18130
|
+
if (!/^[0-9a-f]{40}$/.test(sha)) {
|
|
18131
|
+
log52.warn(TAG48, `refusing to seal "${sha}" as a head anchor (${provenance}) — an anchor is compared against the pull request's own head for equality, so an abbreviated or malformed sha would read as a mismatch rather than as the unreadable head it is`);
|
|
18132
|
+
return;
|
|
18133
|
+
}
|
|
18134
|
+
return sha;
|
|
18135
|
+
}
|
|
18136
|
+
var TAG48 = "head-anchor";
|
|
18137
|
+
var init_head_anchor = () => {};
|
|
18138
|
+
|
|
17703
18139
|
// src/merge-cleanup.ts
|
|
17704
18140
|
import { execFileSync as execFileSync11 } from "node:child_process";
|
|
17705
|
-
import { existsSync as existsSync7, realpathSync as realpathSync2 } from "node:fs";
|
|
18141
|
+
import { existsSync as existsSync7, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs";
|
|
17706
18142
|
import { basename, dirname as dirname8, isAbsolute, resolve as resolve6, sep as sep4 } from "node:path";
|
|
17707
|
-
import { cleanupWorktree as cleanupWorktree6, GIT_NO_HOOKS as GIT_NO_HOOKS12, log as
|
|
18143
|
+
import { cleanupWorktree as cleanupWorktree6, GIT_NO_HOOKS as GIT_NO_HOOKS12, log as log53 } from "@gethmy/harness";
|
|
17708
18144
|
function canonicalize(p) {
|
|
17709
18145
|
const original = isAbsolute(p) ? p : `${process.cwd()}${sep4}${p}`;
|
|
17710
18146
|
try {
|
|
@@ -17731,36 +18167,97 @@ function cwdIsInsideWorktree(cwd, worktreePath) {
|
|
|
17731
18167
|
const b = canonicalize(worktreePath);
|
|
17732
18168
|
return a === b || a.startsWith(b + sep4);
|
|
17733
18169
|
}
|
|
17734
|
-
function
|
|
18170
|
+
function worktreeRecords(cwd) {
|
|
17735
18171
|
let listing;
|
|
17736
18172
|
try {
|
|
17737
18173
|
listing = execFileSync11("git", [...GIT_NO_HOOKS12, "worktree", "list", "--porcelain", "-z"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
17738
18174
|
} catch {
|
|
17739
18175
|
return null;
|
|
17740
18176
|
}
|
|
17741
|
-
|
|
18177
|
+
const records = [];
|
|
18178
|
+
let current = null;
|
|
18179
|
+
for (const field of listing.split("\x00")) {
|
|
18180
|
+
if (field === "") {
|
|
18181
|
+
current = null;
|
|
18182
|
+
continue;
|
|
18183
|
+
}
|
|
18184
|
+
if (field.startsWith("worktree ")) {
|
|
18185
|
+
current = {
|
|
18186
|
+
path: canonicalize(field.slice("worktree ".length)),
|
|
18187
|
+
bare: false
|
|
18188
|
+
};
|
|
18189
|
+
records.push(current);
|
|
18190
|
+
continue;
|
|
18191
|
+
}
|
|
18192
|
+
if (current && field === "bare")
|
|
18193
|
+
current.bare = true;
|
|
18194
|
+
}
|
|
18195
|
+
return records;
|
|
18196
|
+
}
|
|
18197
|
+
function mainWorktreePath(cwd) {
|
|
18198
|
+
try {
|
|
18199
|
+
const out = execFileSync11("git", [...GIT_NO_HOOKS12, "rev-parse", "--git-common-dir"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
18200
|
+
if (!out)
|
|
18201
|
+
return null;
|
|
18202
|
+
return canonicalize(dirname8(resolve6(cwd, out)));
|
|
18203
|
+
} catch {
|
|
18204
|
+
return null;
|
|
18205
|
+
}
|
|
17742
18206
|
}
|
|
17743
|
-
function
|
|
18207
|
+
function linkedWorktreePaths(cwd) {
|
|
18208
|
+
const records = worktreeRecords(cwd);
|
|
18209
|
+
if (records === null)
|
|
18210
|
+
return null;
|
|
18211
|
+
if (records.some((r) => r.bare)) {
|
|
18212
|
+
return records.filter((r) => !r.bare).map((r) => r.path);
|
|
18213
|
+
}
|
|
18214
|
+
const main2 = mainWorktreePath(cwd);
|
|
18215
|
+
if (main2 !== null && records.some((r) => r.path === main2)) {
|
|
18216
|
+
return records.filter((r) => r.path !== main2).map((r) => r.path);
|
|
18217
|
+
}
|
|
18218
|
+
log53.warn(TAG49, `could not identify the main working tree of ${cwd} by path (git derived ${main2 ?? "nothing"} from --git-common-dir, which names no worktree in the listing) — falling back to dropping the first record, which git documents as the main tree. A merge or cleanup that then refuses a worktree it should accept is this fallback, not a forged registration.`);
|
|
18219
|
+
return records.slice(1).map((r) => r.path);
|
|
18220
|
+
}
|
|
18221
|
+
function sameDirectoryIgnoringCase(asked, registered) {
|
|
18222
|
+
try {
|
|
18223
|
+
const a = statSync6(asked);
|
|
18224
|
+
const b = statSync6(registered);
|
|
18225
|
+
return a.dev === b.dev && a.ino === b.ino;
|
|
18226
|
+
} catch {
|
|
18227
|
+
return false;
|
|
18228
|
+
}
|
|
18229
|
+
}
|
|
18230
|
+
function classifyWorktreeRegistration(worktreePath, cwd) {
|
|
17744
18231
|
const registered = linkedWorktreePaths(cwd);
|
|
17745
18232
|
if (registered === null)
|
|
17746
|
-
return
|
|
17747
|
-
|
|
18233
|
+
return { kind: "unanswerable" };
|
|
18234
|
+
const asked = canonicalize(worktreePath);
|
|
18235
|
+
if (registered.includes(asked))
|
|
18236
|
+
return { kind: "registered" };
|
|
18237
|
+
const folded = registered.find((p) => p !== asked && p.toLowerCase() === asked.toLowerCase() && sameDirectoryIgnoringCase(asked, p));
|
|
18238
|
+
if (folded !== undefined) {
|
|
18239
|
+
return { kind: "case-mismatch", registered: folded };
|
|
18240
|
+
}
|
|
18241
|
+
return { kind: "unregistered" };
|
|
18242
|
+
}
|
|
18243
|
+
function isRegisteredWorktree(worktreePath, cwd) {
|
|
18244
|
+
return classifyWorktreeRegistration(worktreePath, cwd).kind === "registered";
|
|
17748
18245
|
}
|
|
17749
18246
|
function cleanupMergedWorktree(args) {
|
|
17750
18247
|
if (!isRegisteredWorktree(args.worktreePath, args.cwd)) {
|
|
17751
18248
|
const reason = `refusing to clean up ${args.worktreePath} — git does not list it as a ` + `worktree of the repository at ${args.cwd}, and removing a path git ` + `does not manage would be a recursive delete of an unknown directory. ` + `Pass the worktree path Step B5 created, and re-run from the ` + `repository root.`;
|
|
17752
|
-
|
|
18249
|
+
log53.warn(TAG49, reason);
|
|
17753
18250
|
return { removed: false, reason };
|
|
17754
18251
|
}
|
|
17755
18252
|
if (cwdIsInsideWorktree(args.cwd, args.worktreePath) || cwdIsInsideWorktree(process.cwd(), args.worktreePath)) {
|
|
17756
18253
|
const reason = `refusing to clean up ${args.worktreePath} from inside it — the branch ` + `delete would run with a deleted cwd and fail silently. Re-run from the ` + `repository root.`;
|
|
17757
|
-
|
|
18254
|
+
log53.warn(TAG49, reason);
|
|
17758
18255
|
return { removed: false, reason };
|
|
17759
18256
|
}
|
|
17760
18257
|
cleanupWorktree6(args.worktreePath, args.branchName ?? undefined);
|
|
17761
18258
|
return { removed: !existsSync7(args.worktreePath) };
|
|
17762
18259
|
}
|
|
17763
|
-
var
|
|
18260
|
+
var TAG49 = "merge-cleanup";
|
|
17764
18261
|
var init_merge_cleanup = () => {};
|
|
17765
18262
|
|
|
17766
18263
|
// src/merge-command.ts
|
|
@@ -17775,7 +18272,7 @@ import { resolve as resolve7 } from "node:path";
|
|
|
17775
18272
|
import { promisify as promisify2 } from "node:util";
|
|
17776
18273
|
import {
|
|
17777
18274
|
GIT_NO_HOOKS as GIT_NO_HOOKS13,
|
|
17778
|
-
log as
|
|
18275
|
+
log as log54,
|
|
17779
18276
|
resolvePrHeadBranch as resolvePrHeadBranch3
|
|
17780
18277
|
} from "@gethmy/harness";
|
|
17781
18278
|
function buildFindingsBlock(items) {
|
|
@@ -17802,7 +18299,7 @@ async function gitRevParse(what, at) {
|
|
|
17802
18299
|
async function cwdIsRepositoryTopLevel(cwd) {
|
|
17803
18300
|
const top = await gitRevParse("--show-toplevel", cwd);
|
|
17804
18301
|
if (!top || realpathOr(top) !== realpathOr(cwd)) {
|
|
17805
|
-
|
|
18302
|
+
log54.warn(TAG50, `not reading a local head from ${cwd} — it is not the top level of a git checkout (git answered ${top ?? "nothing"}). Run merge from the repository root, or pass --worktree.`);
|
|
17806
18303
|
return false;
|
|
17807
18304
|
}
|
|
17808
18305
|
const gitDir = await gitRevParse("--git-dir", cwd);
|
|
@@ -17814,7 +18311,7 @@ async function cwdIsRepositoryTopLevel(cwd) {
|
|
|
17814
18311
|
}
|
|
17815
18312
|
if (isRegisteredWorktree(cwd, cwd))
|
|
17816
18313
|
return true;
|
|
17817
|
-
|
|
18314
|
+
log54.warn(TAG50, `not reading a local head from ${cwd} — git does not list it as a worktree of its own repository.`);
|
|
17818
18315
|
return false;
|
|
17819
18316
|
}
|
|
17820
18317
|
function realpathOr(p) {
|
|
@@ -17824,28 +18321,72 @@ function realpathOr(p) {
|
|
|
17824
18321
|
return p;
|
|
17825
18322
|
}
|
|
17826
18323
|
}
|
|
17827
|
-
async function
|
|
17828
|
-
|
|
17829
|
-
|
|
17830
|
-
|
|
18324
|
+
async function checkedOutBranch(at) {
|
|
18325
|
+
try {
|
|
18326
|
+
const { stdout } = await execFileAsync2("git", [...GIT_NO_HOOKS13, "symbolic-ref", "--quiet", "--short", "HEAD"], { cwd: at });
|
|
18327
|
+
const branch = stdout.trim();
|
|
18328
|
+
return branch === "" ? null : branch;
|
|
18329
|
+
} catch {
|
|
18330
|
+
return null;
|
|
18331
|
+
}
|
|
18332
|
+
}
|
|
18333
|
+
async function readValidatedWorktreeHead(deps) {
|
|
18334
|
+
const { worktreePath, cwd } = deps;
|
|
18335
|
+
if (worktreePath) {
|
|
18336
|
+
const registration = classifyWorktreeRegistration(worktreePath, cwd);
|
|
18337
|
+
if (registration.kind !== "registered") {
|
|
18338
|
+
const detail = registration.kind === "case-mismatch" ? ` The worktree ${worktreePath} is the same directory as the registered ${registration.registered} and differs from it only in case, but git's registration carries the spelling above and this merge matches it exactly. Pass the registered spelling.` : registration.kind === "unanswerable" ? ` The worktree registrations of ${cwd} could not be listed at all, so ${worktreePath} could not be proved to be one of them.` : ` No validated worktree HEAD was available for ${worktreePath} — git does not list it as a worktree of the repository at ${cwd} — so the merge could not be bound to the head this run built and held for a person.`;
|
|
18339
|
+
log54.warn(TAG50, `not reading a local head from ${worktreePath} (${registration.kind}).${detail} The merge will refuse: an interactive merge is only allowed for a head this run can prove it built.`);
|
|
18340
|
+
return { sha: undefined, detail };
|
|
18341
|
+
}
|
|
18342
|
+
}
|
|
18343
|
+
if (!worktreePath && !await cwdIsRepositoryTopLevel(cwd)) {
|
|
18344
|
+
return {
|
|
18345
|
+
sha: undefined,
|
|
18346
|
+
detail: ` No validated local HEAD was available: no --worktree was passed, and ${cwd} is not the top level of a checkout of this repository. The merge could not be bound to the head this run built and held for a person.`
|
|
18347
|
+
};
|
|
17831
18348
|
}
|
|
17832
|
-
if (!worktreePath && !await cwdIsRepositoryTopLevel(cwd))
|
|
17833
|
-
return;
|
|
17834
18349
|
const readAt = worktreePath ?? cwd;
|
|
17835
|
-
const
|
|
17836
|
-
|
|
17837
|
-
|
|
17838
|
-
|
|
17839
|
-
|
|
18350
|
+
const raw = await gitRevParse("HEAD", readAt);
|
|
18351
|
+
const sha = raw ? sealProvenHead(raw, worktreePath ? "registered-worktree" : "repository-top-level") : undefined;
|
|
18352
|
+
if (!sha) {
|
|
18353
|
+
log54.warn(TAG50, `could not read a HEAD sha from ${readAt} (git answered ${raw ? `"${raw}"` : "nothing"}) — the merge will refuse, since it can only run for a head this run can prove it built`);
|
|
18354
|
+
return {
|
|
18355
|
+
sha: undefined,
|
|
18356
|
+
detail: ` No HEAD sha could be read from ${readAt}, so the merge could not be bound to the head this run built and held for a person.`
|
|
18357
|
+
};
|
|
18358
|
+
}
|
|
18359
|
+
if (worktreePath) {
|
|
18360
|
+
const resolved = await resolvePrHeadBranch3(deps.prUrl, cwd, deps.provider);
|
|
18361
|
+
if (resolved.kind === "skip") {
|
|
18362
|
+
log54.warn(TAG50, `not corroborating ${worktreePath} against the head branch of ${deps.prUrl}: ${resolved.reason}. The anchor stands on the head sha alone.`);
|
|
18363
|
+
} else {
|
|
18364
|
+
const branch = await checkedOutBranch(worktreePath);
|
|
18365
|
+
if (branch === null) {
|
|
18366
|
+
log54.warn(TAG50, `${worktreePath} has no branch checked out (detached HEAD), so it could not be corroborated against ${resolved.branch}. The anchor stands on the head sha alone.`);
|
|
18367
|
+
} else if (branch !== resolved.branch) {
|
|
18368
|
+
const detail = ` The worktree ${worktreePath} is on "${branch}", and ${deps.prUrl} has "${resolved.branch}" as its head branch — so that worktree is not where this pull request was built. --worktree is composed from a model's memory of the Step B5 path, which is why it is checked against the pull request and not only against git.`;
|
|
18369
|
+
log54.warn(TAG50, `refusing the anchor from ${worktreePath}.${detail}`);
|
|
18370
|
+
return { sha: undefined, detail };
|
|
18371
|
+
}
|
|
18372
|
+
}
|
|
18373
|
+
}
|
|
18374
|
+
return { sha, detail: "" };
|
|
17840
18375
|
}
|
|
17841
18376
|
async function runMergeCommand(deps) {
|
|
17842
18377
|
const waitMs = deps.config.review.autoMerge.waitMinutes * 60000;
|
|
17843
18378
|
const started = deps.now();
|
|
17844
18379
|
let requestedReview = false;
|
|
17845
|
-
const
|
|
18380
|
+
const anchor = await readValidatedWorktreeHead({
|
|
18381
|
+
worktreePath: deps.worktreePath,
|
|
18382
|
+
cwd: deps.cwd,
|
|
18383
|
+
prUrl: deps.prUrl,
|
|
18384
|
+
provider: deps.provider
|
|
18385
|
+
});
|
|
18386
|
+
const localHeadSha = anchor.sha;
|
|
17846
18387
|
let base = null;
|
|
17847
18388
|
let prHead = null;
|
|
17848
|
-
const anchorDetail =
|
|
18389
|
+
const anchorDetail = anchor.detail;
|
|
17849
18390
|
for (;; ) {
|
|
17850
18391
|
const findings = [];
|
|
17851
18392
|
let outcome;
|
|
@@ -17875,7 +18416,7 @@ async function runMergeCommand(deps) {
|
|
|
17875
18416
|
});
|
|
17876
18417
|
} catch (err) {
|
|
17877
18418
|
const detail = err instanceof Error ? err.message : String(err);
|
|
17878
|
-
|
|
18419
|
+
log54.warn(TAG50, `merge attempt failed: ${detail}`);
|
|
17879
18420
|
deps.emit({
|
|
17880
18421
|
type: "parked",
|
|
17881
18422
|
reason: `the merge attempt failed: ${detail}`
|
|
@@ -17896,7 +18437,7 @@ async function runMergeCommand(deps) {
|
|
|
17896
18437
|
if (outcome === "report-findings") {
|
|
17897
18438
|
const reported = findings[0];
|
|
17898
18439
|
if (!reported) {
|
|
17899
|
-
|
|
18440
|
+
log54.warn(TAG50, "the merge reported findings but none arrived through the callback — parking rather than reporting an empty list");
|
|
17900
18441
|
deps.emit({
|
|
17901
18442
|
type: "parked",
|
|
17902
18443
|
reason: "the independent review posted blocking findings, but they could not be reported — the card stays in Review for a person"
|
|
@@ -17946,7 +18487,7 @@ async function branchRefGone(branchName, cwd) {
|
|
|
17946
18487
|
const stderr = String(failure.stderr ?? "").trim();
|
|
17947
18488
|
if (failure.code === 1 && !stderr)
|
|
17948
18489
|
return true;
|
|
17949
|
-
|
|
18490
|
+
log54.warn(TAG50, `could not determine whether ${branchName} still exists (code=${String(failure.code)}${stderr ? `, ${stderr}` : ""}) — reporting it as possibly present`);
|
|
17950
18491
|
return false;
|
|
17951
18492
|
}
|
|
17952
18493
|
}
|
|
@@ -17957,7 +18498,7 @@ async function finish(deps) {
|
|
|
17957
18498
|
const resolved = await resolvePrHeadBranch3(deps.prUrl, deps.cwd, deps.provider);
|
|
17958
18499
|
const branch = resolved.kind === "branch" ? resolved.branch : null;
|
|
17959
18500
|
if (resolved.kind === "skip") {
|
|
17960
|
-
|
|
18501
|
+
log54.warn(TAG50, `no head branch for ${deps.prUrl}: ${resolved.reason}`);
|
|
17961
18502
|
}
|
|
17962
18503
|
const out = cleanupMergedWorktree({
|
|
17963
18504
|
worktreePath,
|
|
@@ -17966,12 +18507,12 @@ async function finish(deps) {
|
|
|
17966
18507
|
});
|
|
17967
18508
|
if (!out.removed) {
|
|
17968
18509
|
const reason = out.reason ?? "the worktree directory still exists after cleanup";
|
|
17969
|
-
|
|
18510
|
+
log54.warn(TAG50, `cleanup refused for ${worktreePath}: ${reason}`);
|
|
17970
18511
|
deps.emit({ type: "cleanup_refused", worktree: worktreePath, reason });
|
|
17971
18512
|
} else {
|
|
17972
18513
|
const branchGone = branch ? await branchRefGone(branch, deps.cwd) : true;
|
|
17973
18514
|
if (branch && !branchGone) {
|
|
17974
|
-
|
|
18515
|
+
log54.warn(TAG50, `local branch ${branch} may still exist after cleanup`);
|
|
17975
18516
|
}
|
|
17976
18517
|
deps.emit({
|
|
17977
18518
|
type: "cleaned",
|
|
@@ -17981,7 +18522,7 @@ async function finish(deps) {
|
|
|
17981
18522
|
}
|
|
17982
18523
|
} catch (err) {
|
|
17983
18524
|
const detail = err instanceof Error ? err.message : String(err);
|
|
17984
|
-
|
|
18525
|
+
log54.warn(TAG50, `worktree cleanup failed for ${worktreePath}: ${detail}`);
|
|
17985
18526
|
deps.emit({
|
|
17986
18527
|
type: "cleanup_refused",
|
|
17987
18528
|
worktree: worktreePath,
|
|
@@ -17999,19 +18540,20 @@ async function finish(deps) {
|
|
|
17999
18540
|
});
|
|
18000
18541
|
if (!out.moved) {
|
|
18001
18542
|
const reason = out.reason ?? "the card could not be moved out of Review";
|
|
18002
|
-
|
|
18543
|
+
log54.warn(TAG50, `board not updated after merge: ${reason}`);
|
|
18003
18544
|
deps.emit({ type: "board_incomplete", reason });
|
|
18004
18545
|
}
|
|
18005
18546
|
} catch (err) {
|
|
18006
18547
|
const detail = err instanceof Error ? err.message : String(err);
|
|
18007
|
-
|
|
18548
|
+
log54.warn(TAG50, `board not updated after merge: ${detail}`);
|
|
18008
18549
|
deps.emit({ type: "board_incomplete", reason: detail });
|
|
18009
18550
|
}
|
|
18010
18551
|
}
|
|
18011
|
-
var
|
|
18552
|
+
var TAG50 = "merge-command", POLL_INTERVAL_MS = 30000, execFileAsync2, KEEP_WAITING, PARK_REASONS;
|
|
18012
18553
|
var init_merge_command = __esm(() => {
|
|
18013
18554
|
init_dist();
|
|
18014
18555
|
init_auto_merge();
|
|
18556
|
+
init_head_anchor();
|
|
18015
18557
|
init_merge_cleanup();
|
|
18016
18558
|
init_merge_complete();
|
|
18017
18559
|
execFileAsync2 = promisify2(execFile2);
|
|
@@ -18033,10 +18575,118 @@ var init_merge_command = __esm(() => {
|
|
|
18033
18575
|
};
|
|
18034
18576
|
});
|
|
18035
18577
|
|
|
18578
|
+
// src/scan-commands.ts
|
|
18579
|
+
var exports_scan_commands = {};
|
|
18580
|
+
__export(exports_scan_commands, {
|
|
18581
|
+
scanCommandsCommand: () => scanCommandsCommand
|
|
18582
|
+
});
|
|
18583
|
+
import { existsSync as existsSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "node:fs";
|
|
18584
|
+
import { basename as basename2, join as join9 } from "node:path";
|
|
18585
|
+
import {
|
|
18586
|
+
REPO_COMMAND_KEYS,
|
|
18587
|
+
REPO_CONFIG_FILENAMES as REPO_CONFIG_FILENAMES2,
|
|
18588
|
+
renderScanProposal,
|
|
18589
|
+
scanRepoCommands
|
|
18590
|
+
} from "@gethmy/harness";
|
|
18591
|
+
async function scanCommandsCommand(opts = {}) {
|
|
18592
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
18593
|
+
const out = opts.out ?? ((line) => process.stdout.write(`${line}
|
|
18594
|
+
`));
|
|
18595
|
+
const scan = await scanRepoCommands(cwd);
|
|
18596
|
+
out(renderScanProposal(scan));
|
|
18597
|
+
out("");
|
|
18598
|
+
if (!opts.write) {
|
|
18599
|
+
out(`Nothing was written. Re-run with --write to merge this into ${basename2(existingPinPath(cwd))}.`);
|
|
18600
|
+
return 0;
|
|
18601
|
+
}
|
|
18602
|
+
try {
|
|
18603
|
+
const path = writeProposal(cwd, scan);
|
|
18604
|
+
out(`Wrote the commands block to ${path}.`);
|
|
18605
|
+
out("Commit it: a pin git ignores exists in your working directory and in no worktree the daemon cuts.");
|
|
18606
|
+
return 0;
|
|
18607
|
+
} catch (err) {
|
|
18608
|
+
out(`Could not write ${existingPinPath(cwd)}: ${err instanceof Error ? err.message : err}`);
|
|
18609
|
+
return 1;
|
|
18610
|
+
}
|
|
18611
|
+
}
|
|
18612
|
+
function writeProposal(cwd, scan) {
|
|
18613
|
+
const path = existingPinPath(cwd);
|
|
18614
|
+
let existing = {};
|
|
18615
|
+
try {
|
|
18616
|
+
const parsed = JSON.parse(readFileSync10(path, "utf-8"));
|
|
18617
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
18618
|
+
existing = parsed;
|
|
18619
|
+
}
|
|
18620
|
+
} catch {}
|
|
18621
|
+
const merged = {
|
|
18622
|
+
...existing,
|
|
18623
|
+
commands: { ...unscannedCommands(existing, scan), ...scan.proposal }
|
|
18624
|
+
};
|
|
18625
|
+
writeFileSync7(path, `${JSON.stringify(merged, null, indentOf(cwd))}
|
|
18626
|
+
`);
|
|
18627
|
+
return path;
|
|
18628
|
+
}
|
|
18629
|
+
function existingPinPath(cwd) {
|
|
18630
|
+
for (const name of REPO_CONFIG_FILENAMES2) {
|
|
18631
|
+
const candidate = join9(cwd, name);
|
|
18632
|
+
if (existsSync8(candidate))
|
|
18633
|
+
return candidate;
|
|
18634
|
+
}
|
|
18635
|
+
return join9(cwd, PIN_FILENAME);
|
|
18636
|
+
}
|
|
18637
|
+
function unscannedCommands(existing, scan) {
|
|
18638
|
+
const before = existing.commands;
|
|
18639
|
+
if (!before || typeof before !== "object" || Array.isArray(before))
|
|
18640
|
+
return {};
|
|
18641
|
+
const carried = {};
|
|
18642
|
+
for (const key of REPO_COMMAND_KEYS) {
|
|
18643
|
+
if (key in scan.proposal)
|
|
18644
|
+
continue;
|
|
18645
|
+
if (Object.hasOwn(before, key)) {
|
|
18646
|
+
carried[key] = before[key];
|
|
18647
|
+
}
|
|
18648
|
+
}
|
|
18649
|
+
return carried;
|
|
18650
|
+
}
|
|
18651
|
+
function indentOf(cwd) {
|
|
18652
|
+
for (const name of ["biome.json", "biome.jsonc"]) {
|
|
18653
|
+
let parsed;
|
|
18654
|
+
try {
|
|
18655
|
+
parsed = JSON.parse(readFileSync10(join9(cwd, name), "utf-8").replace(/^\s*\/\/.*$/gm, "").replace(/,(\s*[}\]])/g, "$1"));
|
|
18656
|
+
} catch {
|
|
18657
|
+
continue;
|
|
18658
|
+
}
|
|
18659
|
+
if (!parsed || typeof parsed !== "object")
|
|
18660
|
+
continue;
|
|
18661
|
+
const formatter = parsed.formatter;
|
|
18662
|
+
if (formatter?.indentStyle === "space") {
|
|
18663
|
+
const width = formatter.indentWidth;
|
|
18664
|
+
return typeof width === "number" && width > 0 ? width : 2;
|
|
18665
|
+
}
|
|
18666
|
+
return "\t";
|
|
18667
|
+
}
|
|
18668
|
+
try {
|
|
18669
|
+
const editorconfig = readFileSync10(join9(cwd, ".editorconfig"), "utf-8");
|
|
18670
|
+
if (/^\s*indent_style\s*=\s*tab\s*$/im.test(editorconfig))
|
|
18671
|
+
return "\t";
|
|
18672
|
+
const size = editorconfig.match(/^\s*indent_size\s*=\s*(\d+)\s*$/im);
|
|
18673
|
+
if (size) {
|
|
18674
|
+
const width = Number(size[1]);
|
|
18675
|
+
if (width > 0)
|
|
18676
|
+
return width;
|
|
18677
|
+
}
|
|
18678
|
+
} catch {}
|
|
18679
|
+
return 2;
|
|
18680
|
+
}
|
|
18681
|
+
var PIN_FILENAME;
|
|
18682
|
+
var init_scan_commands = __esm(() => {
|
|
18683
|
+
[PIN_FILENAME] = REPO_CONFIG_FILENAMES2;
|
|
18684
|
+
});
|
|
18685
|
+
|
|
18036
18686
|
// src/cli.ts
|
|
18037
18687
|
import { realpathSync as realpathSync4 } from "node:fs";
|
|
18038
18688
|
import { fileURLToPath } from "node:url";
|
|
18039
|
-
import { log as
|
|
18689
|
+
import { log as log55 } from "@gethmy/harness";
|
|
18040
18690
|
var USAGE = `
|
|
18041
18691
|
Harmony Agent — push-based daemon + ops toolkit.
|
|
18042
18692
|
|
|
@@ -18058,6 +18708,11 @@ Usage:
|
|
|
18058
18708
|
to .hmy.json. The old file is KEPT. Exit 1
|
|
18059
18709
|
means the preflight failed OR --fix was
|
|
18060
18710
|
refused; the message says which.
|
|
18711
|
+
harmony-agent scan-commands [--write]
|
|
18712
|
+
Propose this repo's .hmy.json commands block
|
|
18713
|
+
by RUNNING each candidate. Prints only;
|
|
18714
|
+
--write merges it into the pin. A repo with
|
|
18715
|
+
no dev server is a clean result, not an error.
|
|
18061
18716
|
harmony-agent gc One-shot worktree garbage collection
|
|
18062
18717
|
harmony-agent recover One-shot recovery of stranded Review cards
|
|
18063
18718
|
harmony-agent merge --card N [--worktree PATH]
|
|
@@ -18293,7 +18948,10 @@ async function doctorCommand() {
|
|
|
18293
18948
|
const fixCode = process.argv.includes("--fix") ? await fixLocalPinCommand() : 0;
|
|
18294
18949
|
try {
|
|
18295
18950
|
const config = loadDaemonConfig2();
|
|
18296
|
-
validateAutoMergeConfig2(config.agent)
|
|
18951
|
+
for (const warning of validateAutoMergeConfig2(config.agent, config.repoAgentPaths, config.repoArmedGates)) {
|
|
18952
|
+
process.stderr.write(`preflight warning: ${warning}
|
|
18953
|
+
`);
|
|
18954
|
+
}
|
|
18297
18955
|
validateBudgetConfig2(config.agent);
|
|
18298
18956
|
validateSweepConfig2(config.agent);
|
|
18299
18957
|
validateRankingConfig2(config.agent);
|
|
@@ -18311,11 +18969,11 @@ async function doctorCommand() {
|
|
|
18311
18969
|
}
|
|
18312
18970
|
}
|
|
18313
18971
|
async function openStateStoreForOps() {
|
|
18314
|
-
const { existsSync:
|
|
18972
|
+
const { existsSync: existsSync9 } = await import("node:fs");
|
|
18315
18973
|
const { resolveProjectIdForOps: resolveProjectIdForOps2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
18316
18974
|
const { defaultStatePath: defaultStatePath2, StateStore: StateStore2 } = await Promise.resolve().then(() => (init_state_store(), exports_state_store));
|
|
18317
18975
|
const projectId = resolveProjectIdForOps2();
|
|
18318
|
-
if (projectId &&
|
|
18976
|
+
if (projectId && existsSync9(defaultStatePath2(projectId))) {
|
|
18319
18977
|
return StateStore2.open({ projectId });
|
|
18320
18978
|
}
|
|
18321
18979
|
return StateStore2.open();
|
|
@@ -18418,6 +19076,10 @@ merge: accepts ${[...MERGE_FLAGS].join(" ")}
|
|
|
18418
19076
|
}
|
|
18419
19077
|
const { loadDaemonConfig: loadDaemonConfig2, createApiClient: createApiClient2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
18420
19078
|
const config = loadDaemonConfig2();
|
|
19079
|
+
for (const warning of config.configWarnings) {
|
|
19080
|
+
process.stderr.write(`merge: warning: ${warning}
|
|
19081
|
+
`);
|
|
19082
|
+
}
|
|
18421
19083
|
if (!config.agent.review.autoMerge.interactive) {
|
|
18422
19084
|
process.stdout.write(`${JSON.stringify({ type: "disabled" })}
|
|
18423
19085
|
`);
|
|
@@ -18425,7 +19087,10 @@ merge: accepts ${[...MERGE_FLAGS].join(" ")}
|
|
|
18425
19087
|
}
|
|
18426
19088
|
const { validateAutoMergeConfig: validateAutoMergeConfig2, ConfigValidationError: ConfigValidationError2 } = await Promise.resolve().then(() => (init_config_validation(), exports_config_validation));
|
|
18427
19089
|
try {
|
|
18428
|
-
validateAutoMergeConfig2(config.agent)
|
|
19090
|
+
for (const warning of validateAutoMergeConfig2(config.agent, config.repoAgentPaths, config.repoArmedGates)) {
|
|
19091
|
+
process.stderr.write(`merge: warning: ${warning}
|
|
19092
|
+
`);
|
|
19093
|
+
}
|
|
18429
19094
|
} catch (err) {
|
|
18430
19095
|
if (!(err instanceof ConfigValidationError2))
|
|
18431
19096
|
throw err;
|
|
@@ -18601,8 +19266,8 @@ async function runsShowCommand(rest) {
|
|
|
18601
19266
|
`);
|
|
18602
19267
|
let content;
|
|
18603
19268
|
try {
|
|
18604
|
-
const { readFileSync:
|
|
18605
|
-
content =
|
|
19269
|
+
const { readFileSync: readFileSync11 } = await import("node:fs");
|
|
19270
|
+
content = readFileSync11(logFile.path, "utf-8");
|
|
18606
19271
|
} catch (err) {
|
|
18607
19272
|
process.stderr.write(`could not read log: ${err instanceof Error ? err.message : err}
|
|
18608
19273
|
`);
|
|
@@ -18765,6 +19430,10 @@ async function dispatch(argv) {
|
|
|
18765
19430
|
return healthCommand();
|
|
18766
19431
|
case "doctor":
|
|
18767
19432
|
return doctorCommand();
|
|
19433
|
+
case "scan-commands": {
|
|
19434
|
+
const { scanCommandsCommand: scanCommandsCommand2 } = await Promise.resolve().then(() => (init_scan_commands(), exports_scan_commands));
|
|
19435
|
+
return scanCommandsCommand2({ write: args.includes("--write") });
|
|
19436
|
+
}
|
|
18768
19437
|
case "gc":
|
|
18769
19438
|
return gcCommand();
|
|
18770
19439
|
case "recover":
|
|
@@ -18803,7 +19472,7 @@ if (isMainModule()) {
|
|
|
18803
19472
|
if (code !== 0)
|
|
18804
19473
|
process.exit(code);
|
|
18805
19474
|
}).catch((err) => {
|
|
18806
|
-
|
|
19475
|
+
log55.error("cli", err instanceof Error ? err.message : String(err));
|
|
18807
19476
|
process.exit(1);
|
|
18808
19477
|
});
|
|
18809
19478
|
}
|