@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/index.js
CHANGED
|
@@ -851,6 +851,9 @@ function buildGaveUpComment(maxAttempts, failures, pauseEnabled) {
|
|
|
851
851
|
function mergeAgentConfig(defaults, overrides) {
|
|
852
852
|
return mergeObjects(defaults, overrides);
|
|
853
853
|
}
|
|
854
|
+
function own(source, key) {
|
|
855
|
+
return Object.hasOwn(source, key) ? source[key] : undefined;
|
|
856
|
+
}
|
|
854
857
|
function isPlainObject(value) {
|
|
855
858
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
856
859
|
return false;
|
|
@@ -863,7 +866,7 @@ function mergeObjects(base, override) {
|
|
|
863
866
|
for (const key of Object.keys(base)) {
|
|
864
867
|
if (FORBIDDEN_KEYS.has(key))
|
|
865
868
|
continue;
|
|
866
|
-
result[key] = mergeValue(base[key], override
|
|
869
|
+
result[key] = mergeValue(base[key], own(override, key));
|
|
867
870
|
}
|
|
868
871
|
for (const key of Object.keys(override)) {
|
|
869
872
|
if (FORBIDDEN_KEYS.has(key))
|
|
@@ -902,7 +905,7 @@ function walk(base, override, path, found) {
|
|
|
902
905
|
if (FORBIDDEN_KEYS.has(key))
|
|
903
906
|
continue;
|
|
904
907
|
const baseValue = base[key];
|
|
905
|
-
const overrideValue = override
|
|
908
|
+
const overrideValue = own(override, key);
|
|
906
909
|
if (overrideValue === undefined)
|
|
907
910
|
continue;
|
|
908
911
|
const here = `${path}.${key}`;
|
|
@@ -947,7 +950,11 @@ function copy(value) {
|
|
|
947
950
|
}
|
|
948
951
|
var FORBIDDEN_KEYS;
|
|
949
952
|
var init_config_merge = __esm(() => {
|
|
950
|
-
FORBIDDEN_KEYS = new Set([
|
|
953
|
+
FORBIDDEN_KEYS = new Set([
|
|
954
|
+
"__proto__",
|
|
955
|
+
"constructor",
|
|
956
|
+
"prototype"
|
|
957
|
+
]);
|
|
951
958
|
});
|
|
952
959
|
|
|
953
960
|
// src/config-validation.ts
|
|
@@ -962,8 +969,10 @@ __export(exports_config_validation, {
|
|
|
962
969
|
validateBudgetConfig: () => validateBudgetConfig,
|
|
963
970
|
validateAutoMergeConfig: () => validateAutoMergeConfig,
|
|
964
971
|
validateAndListColumns: () => validateAndListColumns,
|
|
972
|
+
repoArmedGatesOf: () => repoArmedGatesOf,
|
|
965
973
|
isLoopbackBindAddr: () => isLoopbackBindAddr,
|
|
966
974
|
findRemovedConfigKeys: () => findRemovedConfigKeys,
|
|
975
|
+
REPO_ARMABLE_GATES: () => REPO_ARMABLE_GATES,
|
|
967
976
|
REMOVED_CONFIG_KEYS: () => REMOVED_CONFIG_KEYS,
|
|
968
977
|
ConfigValidationError: () => ConfigValidationError
|
|
969
978
|
});
|
|
@@ -978,7 +987,7 @@ function findRemovedConfigKeys(rawConfig) {
|
|
|
978
987
|
cursor = undefined;
|
|
979
988
|
break;
|
|
980
989
|
}
|
|
981
|
-
cursor = cursor
|
|
990
|
+
cursor = own(cursor, segment);
|
|
982
991
|
}
|
|
983
992
|
if (cursor !== undefined) {
|
|
984
993
|
found.push(`${path} is no longer used (#988) — ${note}. Remove it.`);
|
|
@@ -1011,9 +1020,49 @@ function validateVerificationConfig(config) {
|
|
|
1011
1020
|
- `)}`, issues);
|
|
1012
1021
|
}
|
|
1013
1022
|
}
|
|
1014
|
-
function
|
|
1023
|
+
function repoArmedGatesOf(suppliedPaths, operatorAgent, merged) {
|
|
1024
|
+
const armed = new Set;
|
|
1025
|
+
for (const gate of Object.keys(REPO_ARMABLE_GATES)) {
|
|
1026
|
+
if (!suppliedPaths.has(gate))
|
|
1027
|
+
continue;
|
|
1028
|
+
if (ownValueAt(operatorAgent, gate))
|
|
1029
|
+
continue;
|
|
1030
|
+
if (ownValueAt(merged, gate) !== true)
|
|
1031
|
+
continue;
|
|
1032
|
+
armed.add(gate);
|
|
1033
|
+
}
|
|
1034
|
+
return armed;
|
|
1035
|
+
}
|
|
1036
|
+
function ownValueAt(root, dotted) {
|
|
1037
|
+
let cursor = root;
|
|
1038
|
+
for (const segment of dotted.split(".")) {
|
|
1039
|
+
if (cursor === null || typeof cursor !== "object" || !Object.hasOwn(cursor, segment)) {
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
cursor = cursor[segment];
|
|
1043
|
+
}
|
|
1044
|
+
return cursor;
|
|
1045
|
+
}
|
|
1046
|
+
function armedByRepoPin(gate) {
|
|
1047
|
+
if (REPO_ARMABLE_GATES[gate] === "tightening") {
|
|
1048
|
+
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).`;
|
|
1049
|
+
}
|
|
1050
|
+
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.`;
|
|
1051
|
+
}
|
|
1052
|
+
function validateAutoMergeConfig(config, repoSuppliedPaths = NO_REPO_PATHS, repoArmedGates = NO_REPO_PATHS) {
|
|
1015
1053
|
const autoMerge = config.review.autoMerge;
|
|
1016
1054
|
const issues = [];
|
|
1055
|
+
const warnings = [];
|
|
1056
|
+
const armedRefusal = (gate, issue) => {
|
|
1057
|
+
issues.push(repoArmedGates.has(gate) ? `${issue}. ${armedByRepoPin(gate)}` : issue);
|
|
1058
|
+
};
|
|
1059
|
+
const inertCombination = (args) => {
|
|
1060
|
+
if (repoSuppliedPaths.has(args.gate)) {
|
|
1061
|
+
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.`);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
armedRefusal(args.pin, args.issue);
|
|
1065
|
+
};
|
|
1017
1066
|
const valid = ["squash", "merge", "rebase"];
|
|
1018
1067
|
const s = autoMerge.strategy;
|
|
1019
1068
|
if (!valid.includes(s)) {
|
|
@@ -1022,34 +1071,44 @@ function validateAutoMergeConfig(config) {
|
|
|
1022
1071
|
const repair = autoMerge.ciRepair;
|
|
1023
1072
|
if (repair.enabled) {
|
|
1024
1073
|
if (!autoMerge.enabled) {
|
|
1025
|
-
|
|
1074
|
+
inertCombination({
|
|
1075
|
+
pin: "review.autoMerge.ciRepair.enabled",
|
|
1076
|
+
gate: "review.autoMerge.enabled",
|
|
1077
|
+
issue: "review.autoMerge.ciRepair.enabled: needs review.autoMerge.enabled — a repair only runs on a card the daemon would merge itself"
|
|
1078
|
+
});
|
|
1026
1079
|
}
|
|
1027
1080
|
if (!autoMerge.requireGreenCi) {
|
|
1028
|
-
|
|
1081
|
+
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");
|
|
1029
1082
|
}
|
|
1030
1083
|
if (!Number.isInteger(repair.maxAttempts) || repair.maxAttempts < 1) {
|
|
1031
|
-
|
|
1084
|
+
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`);
|
|
1032
1085
|
}
|
|
1033
1086
|
}
|
|
1034
1087
|
const patch = repair.patch;
|
|
1035
1088
|
if (patch?.enabled) {
|
|
1036
1089
|
if (!repair.enabled) {
|
|
1037
|
-
|
|
1090
|
+
inertCombination({
|
|
1091
|
+
pin: "review.autoMerge.ciRepair.patch.enabled",
|
|
1092
|
+
gate: "review.autoMerge.ciRepair.enabled",
|
|
1093
|
+
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"
|
|
1094
|
+
});
|
|
1038
1095
|
}
|
|
1039
1096
|
if (!autoMerge.reReviewOnBranchChange) {
|
|
1040
|
-
|
|
1097
|
+
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");
|
|
1041
1098
|
}
|
|
1042
|
-
|
|
1099
|
+
for (const issue of sandboxConfigIssues({
|
|
1043
1100
|
image: patch.sandboxImage,
|
|
1044
1101
|
timeoutMs: patch.sandboxTimeoutMs,
|
|
1045
1102
|
imagePath: "review.autoMerge.ciRepair.patch.sandboxImage",
|
|
1046
1103
|
timeoutPath: "review.autoMerge.ciRepair.patch.sandboxTimeoutMs"
|
|
1047
|
-
}))
|
|
1104
|
+
})) {
|
|
1105
|
+
armedRefusal("review.autoMerge.ciRepair.patch.enabled", issue);
|
|
1106
|
+
}
|
|
1048
1107
|
if (!Number.isInteger(patch.maxTurns) || patch.maxTurns < 1) {
|
|
1049
|
-
|
|
1108
|
+
armedRefusal("review.autoMerge.ciRepair.patch.enabled", `review.autoMerge.ciRepair.patch.maxTurns: must be an integer >= 1 (got ${patch.maxTurns})`);
|
|
1050
1109
|
}
|
|
1051
1110
|
if (!(patch.maxBudgetUsd > 0)) {
|
|
1052
|
-
|
|
1111
|
+
armedRefusal("review.autoMerge.ciRepair.patch.enabled", `review.autoMerge.ciRepair.patch.maxBudgetUsd: must be greater than 0 (got ${patch.maxBudgetUsd})`);
|
|
1053
1112
|
}
|
|
1054
1113
|
}
|
|
1055
1114
|
const independent = autoMerge.independentReview;
|
|
@@ -1058,38 +1117,46 @@ function validateAutoMergeConfig(config) {
|
|
|
1058
1117
|
}
|
|
1059
1118
|
if (independent?.enabled) {
|
|
1060
1119
|
if (typeof independent.checkName !== "string" || !independent.checkName.trim()) {
|
|
1061
|
-
|
|
1120
|
+
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");
|
|
1062
1121
|
}
|
|
1063
1122
|
if (typeof independent.label !== "string" || !independent.label.trim()) {
|
|
1064
|
-
|
|
1123
|
+
armedRefusal("review.autoMerge.independentReview.enabled", "review.autoMerge.independentReview.label: must name the PR label that requests a CI review");
|
|
1065
1124
|
}
|
|
1066
1125
|
const verdict = independent.verdict;
|
|
1067
1126
|
if (typeof verdict?.enabled !== "boolean") {
|
|
1068
|
-
|
|
1127
|
+
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`);
|
|
1069
1128
|
}
|
|
1070
1129
|
if (typeof verdict?.checkName !== "string" || !verdict.checkName.trim()) {
|
|
1071
|
-
|
|
1130
|
+
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");
|
|
1072
1131
|
}
|
|
1073
1132
|
if (typeof independent.checkName === "string" && typeof verdict?.checkName === "string" && independent.checkName.trim().toLowerCase() === verdict.checkName.trim().toLowerCase() && independent.checkName.trim()) {
|
|
1074
|
-
|
|
1133
|
+
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`);
|
|
1075
1134
|
}
|
|
1076
1135
|
}
|
|
1077
1136
|
if (typeof autoMerge.interactive !== "boolean") {
|
|
1078
1137
|
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`);
|
|
1079
1138
|
}
|
|
1139
|
+
if (autoMerge.interactive || autoMerge.enabled) {
|
|
1140
|
+
const driver = autoMerge.interactive ? "review.autoMerge.interactive" : "review.autoMerge.enabled";
|
|
1141
|
+
for (const named of REPO_UNSETTABLE_CHECK_NAMES) {
|
|
1142
|
+
if (!repoSuppliedPaths.has(named))
|
|
1143
|
+
continue;
|
|
1144
|
+
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.`);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1080
1147
|
if (autoMerge.interactive) {
|
|
1081
1148
|
if (independent?.enabled === false) {
|
|
1082
|
-
|
|
1149
|
+
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");
|
|
1083
1150
|
}
|
|
1084
1151
|
if (!autoMerge.reReviewOnBranchChange) {
|
|
1085
|
-
|
|
1152
|
+
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");
|
|
1086
1153
|
}
|
|
1087
1154
|
if (independent?.enabled && independent.verdict?.enabled === false) {
|
|
1088
|
-
|
|
1155
|
+
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)");
|
|
1089
1156
|
}
|
|
1090
1157
|
const w = autoMerge.waitMinutes;
|
|
1091
1158
|
if (!Number.isInteger(w) || w < 1 || w > 55) {
|
|
1092
|
-
|
|
1159
|
+
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`);
|
|
1093
1160
|
}
|
|
1094
1161
|
}
|
|
1095
1162
|
if (typeof autoMerge.findingsLoop !== "boolean") {
|
|
@@ -1101,16 +1168,21 @@ function validateAutoMergeConfig(config) {
|
|
|
1101
1168
|
}
|
|
1102
1169
|
if (autoMerge.findingsLoop) {
|
|
1103
1170
|
if (!autoMerge.interactive) {
|
|
1104
|
-
|
|
1171
|
+
inertCombination({
|
|
1172
|
+
pin: "review.autoMerge.findingsLoop",
|
|
1173
|
+
gate: "review.autoMerge.interactive",
|
|
1174
|
+
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"
|
|
1175
|
+
});
|
|
1105
1176
|
}
|
|
1106
1177
|
const findingAuthors = independent?.findingAuthors;
|
|
1107
1178
|
if (!Array.isArray(findingAuthors) || findingAuthors.filter((a) => typeof a === "string" && a.trim()).length === 0) {
|
|
1108
|
-
|
|
1179
|
+
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");
|
|
1109
1180
|
}
|
|
1110
1181
|
}
|
|
1111
1182
|
if (issues.length > 0) {
|
|
1112
1183
|
throw new ConfigValidationError(`Invalid agent config — ${issues.join("; ")}`, issues);
|
|
1113
1184
|
}
|
|
1185
|
+
return warnings;
|
|
1114
1186
|
}
|
|
1115
1187
|
function isLoopbackBindAddr(addr) {
|
|
1116
1188
|
const value = addr.toLowerCase();
|
|
@@ -1332,8 +1404,9 @@ async function validateAndListColumns(client, projectId, config) {
|
|
|
1332
1404
|
}
|
|
1333
1405
|
return Array.from(new Set(names.filter(Boolean)));
|
|
1334
1406
|
}
|
|
1335
|
-
var ConfigValidationError, REMOVED_CONFIG_KEYS;
|
|
1407
|
+
var ConfigValidationError, REMOVED_CONFIG_KEYS, NO_REPO_PATHS, REPO_UNSETTABLE_CHECK_NAMES, REPO_ARMABLE_GATES;
|
|
1336
1408
|
var init_config_validation = __esm(() => {
|
|
1409
|
+
init_config_merge();
|
|
1337
1410
|
ConfigValidationError = class ConfigValidationError extends Error {
|
|
1338
1411
|
issues;
|
|
1339
1412
|
constructor(message, issues) {
|
|
@@ -1351,6 +1424,18 @@ var init_config_validation = __esm(() => {
|
|
|
1351
1424
|
note: "review, auto-fix and deep-review are contained and pin their own sources"
|
|
1352
1425
|
}
|
|
1353
1426
|
];
|
|
1427
|
+
NO_REPO_PATHS = new Set;
|
|
1428
|
+
REPO_UNSETTABLE_CHECK_NAMES = [
|
|
1429
|
+
"review.autoMerge.independentReview.checkName",
|
|
1430
|
+
"review.autoMerge.independentReview.verdict.checkName"
|
|
1431
|
+
];
|
|
1432
|
+
REPO_ARMABLE_GATES = {
|
|
1433
|
+
"review.autoMerge.independentReview.enabled": "tightening",
|
|
1434
|
+
"review.autoMerge.interactive": "handshake",
|
|
1435
|
+
"review.autoMerge.ciRepair.enabled": "handshake",
|
|
1436
|
+
"review.autoMerge.ciRepair.patch.enabled": "handshake",
|
|
1437
|
+
"review.autoMerge.findingsLoop": "handshake"
|
|
1438
|
+
};
|
|
1354
1439
|
});
|
|
1355
1440
|
|
|
1356
1441
|
// src/paths.ts
|
|
@@ -1497,6 +1582,9 @@ var init_branchRef = __esm(() => {
|
|
|
1497
1582
|
PR_LINK_PATTERN = /PR:\s*(https?:\/\/[^\s)]+)/;
|
|
1498
1583
|
});
|
|
1499
1584
|
|
|
1585
|
+
// ../harmony-shared/dist/cardLifecycle.js
|
|
1586
|
+
var init_cardLifecycle = () => {};
|
|
1587
|
+
|
|
1500
1588
|
// ../harmony-shared/dist/cardLinks.js
|
|
1501
1589
|
var init_cardLinks = () => {};
|
|
1502
1590
|
|
|
@@ -2985,6 +3073,7 @@ function untrustedDataBlock(text, options) {
|
|
|
2985
3073
|
var init_dist = __esm(() => {
|
|
2986
3074
|
init_agentStaleness();
|
|
2987
3075
|
init_branchRef();
|
|
3076
|
+
init_cardLifecycle();
|
|
2988
3077
|
init_cardLinks();
|
|
2989
3078
|
init_cardModelStat();
|
|
2990
3079
|
init_classification();
|
|
@@ -3348,16 +3437,17 @@ var init_types2 = __esm(() => {
|
|
|
3348
3437
|
},
|
|
3349
3438
|
claude: {
|
|
3350
3439
|
model: "claude-opus-5",
|
|
3351
|
-
escalateModel: "claude-fable-5",
|
|
3440
|
+
escalateModel: "claude-fable-5-1",
|
|
3352
3441
|
escalateAfterAttempts: 2,
|
|
3353
3442
|
tiers: {
|
|
3354
3443
|
simple: "claude-sonnet-5",
|
|
3355
3444
|
advanced: "claude-opus-5",
|
|
3356
|
-
research: "claude-fable-5"
|
|
3445
|
+
research: "claude-fable-5-1"
|
|
3357
3446
|
},
|
|
3358
3447
|
scoreBands: DEFAULT_SCORE_BANDS,
|
|
3359
3448
|
sizingModel: "",
|
|
3360
3449
|
reviewModel: "sonnet",
|
|
3450
|
+
deepReviewModel: "sonnet",
|
|
3361
3451
|
maxTurns: 240,
|
|
3362
3452
|
reviewMaxTurns: 60,
|
|
3363
3453
|
additionalArgs: []
|
|
@@ -3472,6 +3562,302 @@ var init_types2 = __esm(() => {
|
|
|
3472
3562
|
NOTICE_IDENTIFIER = `${DAEMON_IDENTIFIER_BASE}-notice`;
|
|
3473
3563
|
});
|
|
3474
3564
|
|
|
3565
|
+
// src/repo-agent-config.ts
|
|
3566
|
+
import { readFileSync } from "node:fs";
|
|
3567
|
+
import { findLocalConfigPath } from "@gethmy/mcp/src/config.js";
|
|
3568
|
+
function isNeverSettable(path) {
|
|
3569
|
+
for (const banned of REPO_NEVER_SETTABLE) {
|
|
3570
|
+
if (path === banned || path.startsWith(`${banned}.`))
|
|
3571
|
+
return true;
|
|
3572
|
+
}
|
|
3573
|
+
return false;
|
|
3574
|
+
}
|
|
3575
|
+
function renderForWarning(value) {
|
|
3576
|
+
const json = JSON.stringify(clampForRender(value)) ?? String(value);
|
|
3577
|
+
const clean = json.replace(/\p{Cf}/gu, "").replace(/[\p{Cc}\u2028\u2029]/gu, " ");
|
|
3578
|
+
if (clean.length <= MAX_RENDERED_LENGTH)
|
|
3579
|
+
return clean;
|
|
3580
|
+
const head = clean.slice(0, MAX_RENDERED_LENGTH).replace(/\\u[0-9a-fA-F]{0,3}$/, "").replace(/\\+$/, "").replace(/[\uD800-\uDBFF]$/, "");
|
|
3581
|
+
const closer = clean.startsWith('"') ? '"' : "";
|
|
3582
|
+
return `${head}…${closer} (truncated from ${clean.length} characters)`;
|
|
3583
|
+
}
|
|
3584
|
+
function clampForRender(value, depth = 0) {
|
|
3585
|
+
if (Array.isArray(value)) {
|
|
3586
|
+
if (depth >= MAX_RENDER_DEPTH)
|
|
3587
|
+
return ELISION;
|
|
3588
|
+
const kept = value.slice(0, MAX_RENDER_ENTRIES).map((item) => clampForRender(item, depth + 1));
|
|
3589
|
+
if (value.length > MAX_RENDER_ENTRIES)
|
|
3590
|
+
kept.push(ELISION);
|
|
3591
|
+
return kept;
|
|
3592
|
+
}
|
|
3593
|
+
if (isPlainObject(value)) {
|
|
3594
|
+
if (depth >= MAX_RENDER_DEPTH)
|
|
3595
|
+
return ELISION;
|
|
3596
|
+
const keys = Object.keys(value);
|
|
3597
|
+
const out = Object.create(null);
|
|
3598
|
+
for (const key of keys.slice(0, MAX_RENDER_ENTRIES)) {
|
|
3599
|
+
out[key] = clampForRender(value[key], depth + 1);
|
|
3600
|
+
}
|
|
3601
|
+
return out;
|
|
3602
|
+
}
|
|
3603
|
+
return value;
|
|
3604
|
+
}
|
|
3605
|
+
function describeParent(drop) {
|
|
3606
|
+
return drop.parent ? ` inside ${renderForWarning(`agent.${drop.parent}`)}` : ` inside "agent"`;
|
|
3607
|
+
}
|
|
3608
|
+
function isOneSegment(key) {
|
|
3609
|
+
return key.length > 0 && !key.includes(".") && !UNPRINTABLE.test(key);
|
|
3610
|
+
}
|
|
3611
|
+
function refuseValue(value, depth = 0) {
|
|
3612
|
+
if (typeof value === "string") {
|
|
3613
|
+
if (UNPRINTABLE.test(value))
|
|
3614
|
+
return `A repo-supplied value may not hold a control or invisible character, and nothing in ${ALLOWED_PREFIX}* is spelled with one.`;
|
|
3615
|
+
if (value.length > MAX_VALUE_LENGTH)
|
|
3616
|
+
return `A repo-supplied value may be at most ${MAX_VALUE_LENGTH} characters, and this one is ${value.length}.`;
|
|
3617
|
+
return null;
|
|
3618
|
+
}
|
|
3619
|
+
if (depth >= MAX_VALUE_DEPTH)
|
|
3620
|
+
return `A repo-supplied value may not nest more than ${MAX_VALUE_DEPTH} levels deep.`;
|
|
3621
|
+
if (Array.isArray(value)) {
|
|
3622
|
+
for (const item of value) {
|
|
3623
|
+
const why = refuseValue(item, depth + 1);
|
|
3624
|
+
if (why !== null)
|
|
3625
|
+
return why;
|
|
3626
|
+
}
|
|
3627
|
+
return null;
|
|
3628
|
+
}
|
|
3629
|
+
if (isPlainObject(value)) {
|
|
3630
|
+
for (const [key, item] of Object.entries(value)) {
|
|
3631
|
+
const why = refuseValue(key, depth + 1) ?? refuseValue(item, depth + 1);
|
|
3632
|
+
if (why !== null)
|
|
3633
|
+
return why;
|
|
3634
|
+
}
|
|
3635
|
+
return null;
|
|
3636
|
+
}
|
|
3637
|
+
return null;
|
|
3638
|
+
}
|
|
3639
|
+
function flatten(value, prefix, out, dropped, depth = 0) {
|
|
3640
|
+
if (!isPlainObject(value)) {
|
|
3641
|
+
out.set(prefix, value);
|
|
3642
|
+
return;
|
|
3643
|
+
}
|
|
3644
|
+
if (depth >= MAX_BLOCK_DEPTH) {
|
|
3645
|
+
dropped.push({
|
|
3646
|
+
path: prefix,
|
|
3647
|
+
key: lastSegment(prefix),
|
|
3648
|
+
parent: parentPath(prefix),
|
|
3649
|
+
why: "depth"
|
|
3650
|
+
});
|
|
3651
|
+
return;
|
|
3652
|
+
}
|
|
3653
|
+
const keys = Object.keys(value);
|
|
3654
|
+
if (keys.length === 0)
|
|
3655
|
+
return;
|
|
3656
|
+
for (const key of keys) {
|
|
3657
|
+
const here = prefix ? `${prefix}.${key}` : key;
|
|
3658
|
+
if (FORBIDDEN_KEYS.has(key)) {
|
|
3659
|
+
dropped.push({ path: here, key, parent: prefix, why: "reserved" });
|
|
3660
|
+
continue;
|
|
3661
|
+
}
|
|
3662
|
+
if (!isOneSegment(key)) {
|
|
3663
|
+
dropped.push({
|
|
3664
|
+
path: here,
|
|
3665
|
+
key,
|
|
3666
|
+
parent: prefix,
|
|
3667
|
+
why: UNPRINTABLE.test(key) ? "unprintable" : "segment"
|
|
3668
|
+
});
|
|
3669
|
+
continue;
|
|
3670
|
+
}
|
|
3671
|
+
flatten(value[key], here, out, dropped, depth + 1);
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
function lastSegment(path) {
|
|
3675
|
+
const at = path.lastIndexOf(".");
|
|
3676
|
+
return at === -1 ? path : path.slice(at + 1);
|
|
3677
|
+
}
|
|
3678
|
+
function parentPath(path) {
|
|
3679
|
+
const at = path.lastIndexOf(".");
|
|
3680
|
+
return at === -1 ? "" : path.slice(0, at);
|
|
3681
|
+
}
|
|
3682
|
+
function setPath(target, path, value) {
|
|
3683
|
+
const parts = path.split(".");
|
|
3684
|
+
if (parts.some((part) => FORBIDDEN_KEYS.has(part) || !isOneSegment(part)))
|
|
3685
|
+
return false;
|
|
3686
|
+
let node = target;
|
|
3687
|
+
for (const part of parts.slice(0, -1)) {
|
|
3688
|
+
const existing = own(node, part);
|
|
3689
|
+
if (!isPlainObject(existing))
|
|
3690
|
+
node[part] = {};
|
|
3691
|
+
node = node[part];
|
|
3692
|
+
}
|
|
3693
|
+
node[parts[parts.length - 1]] = value;
|
|
3694
|
+
return true;
|
|
3695
|
+
}
|
|
3696
|
+
function describeDrop(pinPath, drop) {
|
|
3697
|
+
switch (drop.why) {
|
|
3698
|
+
case "reserved":
|
|
3699
|
+
return `${pinPath}: ${renderForWarning(`agent.${drop.path}`)} uses a reserved JavaScript key name — ignoring it. Nothing in ${ALLOWED_PREFIX}* is spelled that way.`;
|
|
3700
|
+
case "unprintable":
|
|
3701
|
+
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.`;
|
|
3702
|
+
case "depth":
|
|
3703
|
+
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.`;
|
|
3704
|
+
default:
|
|
3705
|
+
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 ".".`;
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
function describeValue(value) {
|
|
3709
|
+
if (value === null)
|
|
3710
|
+
return "null";
|
|
3711
|
+
if (Array.isArray(value))
|
|
3712
|
+
return "a list";
|
|
3713
|
+
if (isPlainObject(value))
|
|
3714
|
+
return "a block of settings";
|
|
3715
|
+
return `a ${typeof value}`;
|
|
3716
|
+
}
|
|
3717
|
+
function mismatchedPath(message) {
|
|
3718
|
+
return message.slice(0, message.indexOf(" is "));
|
|
3719
|
+
}
|
|
3720
|
+
function readRepoAgentConfig(args) {
|
|
3721
|
+
const nothing = {
|
|
3722
|
+
overrides: {},
|
|
3723
|
+
warnings: [],
|
|
3724
|
+
suppliedPaths: new Set
|
|
3725
|
+
};
|
|
3726
|
+
const pinPath = findLocalConfigPath(args.repoRoot);
|
|
3727
|
+
if (pinPath === null)
|
|
3728
|
+
return nothing;
|
|
3729
|
+
let parsed;
|
|
3730
|
+
try {
|
|
3731
|
+
parsed = JSON.parse(readFileSync(pinPath, "utf-8"));
|
|
3732
|
+
} catch {
|
|
3733
|
+
return nothing;
|
|
3734
|
+
}
|
|
3735
|
+
if (!isPlainObject(parsed))
|
|
3736
|
+
return nothing;
|
|
3737
|
+
const warnings = [];
|
|
3738
|
+
for (const key of Object.keys(parsed)) {
|
|
3739
|
+
if (KNOWN_PIN_KEYS.includes(key))
|
|
3740
|
+
continue;
|
|
3741
|
+
warnings.push(`${pinPath}: ${renderForWarning(key)} is not a key anything reads — ignoring it. Known keys: ${KNOWN_PIN_KEYS.join(", ")}`);
|
|
3742
|
+
}
|
|
3743
|
+
const block = own(parsed, "agent");
|
|
3744
|
+
if (block !== undefined && !isPlainObject(block)) {
|
|
3745
|
+
warnings.push(`${pinPath}: "agent" is ${describeValue(block)} — it must be a block of ${ALLOWED_PREFIX}* settings. Ignoring it.`);
|
|
3746
|
+
}
|
|
3747
|
+
if (!isPlainObject(block))
|
|
3748
|
+
return { overrides: {}, warnings, suppliedPaths: new Set };
|
|
3749
|
+
const leaves = new Map;
|
|
3750
|
+
const dropped = [];
|
|
3751
|
+
flatten(block, "", leaves, dropped);
|
|
3752
|
+
for (const drop of dropped) {
|
|
3753
|
+
warnings.push(describeDrop(pinPath, drop));
|
|
3754
|
+
}
|
|
3755
|
+
const inScope = [];
|
|
3756
|
+
const candidate = {};
|
|
3757
|
+
for (const [path, value] of leaves) {
|
|
3758
|
+
if (path === ALLOWED_BLOCK) {
|
|
3759
|
+
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.`);
|
|
3760
|
+
continue;
|
|
3761
|
+
}
|
|
3762
|
+
if (!path.startsWith(ALLOWED_PREFIX)) {
|
|
3763
|
+
warnings.push(`${pinPath}: ${renderForWarning(`agent.${path}`)} is global config, not repo config — ignoring it. A repository may set ${ALLOWED_PREFIX}* and nothing else.`);
|
|
3764
|
+
continue;
|
|
3765
|
+
}
|
|
3766
|
+
if (isNeverSettable(path)) {
|
|
3767
|
+
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.`);
|
|
3768
|
+
continue;
|
|
3769
|
+
}
|
|
3770
|
+
const badValue = refuseValue(value);
|
|
3771
|
+
if (badValue !== null) {
|
|
3772
|
+
warnings.push(`${pinPath}: ${renderForWarning(`agent.${path}`)} = ${renderForWarning(value)} is not a value a repository may set — ignoring it. ${badValue}`);
|
|
3773
|
+
continue;
|
|
3774
|
+
}
|
|
3775
|
+
if (!setPath(candidate, path, value)) {
|
|
3776
|
+
warnings.push(`${pinPath}: ${renderForWarning(`agent.${path}`)} contains a reserved JavaScript key name — ignoring it. Nothing in ${ALLOWED_PREFIX}* is spelled that way.`);
|
|
3777
|
+
continue;
|
|
3778
|
+
}
|
|
3779
|
+
inScope.push([path, value]);
|
|
3780
|
+
}
|
|
3781
|
+
const malformed = [];
|
|
3782
|
+
for (const message of findConfigTypeMismatches(DEFAULT_AGENT_CONFIG, candidate)) {
|
|
3783
|
+
warnings.push(`${pinPath}: ${message}`);
|
|
3784
|
+
malformed.push(mismatchedPath(message).replace(/^agent\./, ""));
|
|
3785
|
+
}
|
|
3786
|
+
const kept = {};
|
|
3787
|
+
const suppliedPaths = new Set;
|
|
3788
|
+
for (const [path, value] of inScope) {
|
|
3789
|
+
if (malformed.some((bad) => path === bad || path.startsWith(`${bad}.`)))
|
|
3790
|
+
continue;
|
|
3791
|
+
if (!args.allowRepoOverrides && FREE_VALUE.get(path) !== value) {
|
|
3792
|
+
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.`);
|
|
3793
|
+
continue;
|
|
3794
|
+
}
|
|
3795
|
+
if (!setPath(kept, path, value))
|
|
3796
|
+
continue;
|
|
3797
|
+
suppliedPaths.add(path);
|
|
3798
|
+
const named = NAMED_REVIEW_PATHS.get(path);
|
|
3799
|
+
if (named !== undefined) {
|
|
3800
|
+
warnings.push(`${pinPath}: ${renderForWarning(`agent.${path}`)} = ${renderForWarning(value)} — ${named.effect} Accepted because "allowRepoOverrides": true is set in ${args.globalConfigPath}.${named.extra ? ` ${named.extra}` : ""}`);
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
return {
|
|
3804
|
+
overrides: kept,
|
|
3805
|
+
warnings,
|
|
3806
|
+
suppliedPaths
|
|
3807
|
+
};
|
|
3808
|
+
}
|
|
3809
|
+
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;
|
|
3810
|
+
var init_repo_agent_config = __esm(() => {
|
|
3811
|
+
init_config_merge();
|
|
3812
|
+
init_types2();
|
|
3813
|
+
KNOWN_PIN_KEYS = [
|
|
3814
|
+
"workspaceId",
|
|
3815
|
+
"projectId",
|
|
3816
|
+
"commands",
|
|
3817
|
+
"agent"
|
|
3818
|
+
];
|
|
3819
|
+
ALLOWED_BLOCK = ALLOWED_PREFIX.slice(0, -1);
|
|
3820
|
+
FREE_VALUE = new Map([
|
|
3821
|
+
["review.autoMerge.enabled", false],
|
|
3822
|
+
["review.autoMerge.interactive", false],
|
|
3823
|
+
["review.autoMerge.findingsLoop", false],
|
|
3824
|
+
["review.autoMerge.ciRepair.enabled", false],
|
|
3825
|
+
["review.autoMerge.ciRepair.patch.enabled", false],
|
|
3826
|
+
["review.autoMerge.requireGreenCi", true],
|
|
3827
|
+
["review.autoMerge.reReviewOnBranchChange", true],
|
|
3828
|
+
["review.autoMerge.independentReview.enabled", true],
|
|
3829
|
+
["review.autoMerge.independentReview.verdict.enabled", true]
|
|
3830
|
+
]);
|
|
3831
|
+
REPO_NEVER_SETTABLE = new Set([
|
|
3832
|
+
"review.autoMerge.ciRepair.patch.sandboxImage",
|
|
3833
|
+
"review.autoMerge.ciRepair.patch.sandboxTimeoutMs"
|
|
3834
|
+
]);
|
|
3835
|
+
NAMED_REVIEW_PATHS = new Map([
|
|
3836
|
+
[
|
|
3837
|
+
"review.autoMerge.independentReview.checkName",
|
|
3838
|
+
{
|
|
3839
|
+
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.",
|
|
3840
|
+
extra: `While ${ALLOWED_PREFIX}interactive or ${ALLOWED_PREFIX}enabled is on, a repo-supplied check name is refused at startup.`
|
|
3841
|
+
}
|
|
3842
|
+
],
|
|
3843
|
+
[
|
|
3844
|
+
"review.autoMerge.independentReview.verdict.checkName",
|
|
3845
|
+
{
|
|
3846
|
+
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.",
|
|
3847
|
+
extra: `While ${ALLOWED_PREFIX}interactive or ${ALLOWED_PREFIX}enabled is on, a repo-supplied check name is refused at startup.`
|
|
3848
|
+
}
|
|
3849
|
+
],
|
|
3850
|
+
[
|
|
3851
|
+
"review.autoMerge.independentReview.label",
|
|
3852
|
+
{
|
|
3853
|
+
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.",
|
|
3854
|
+
extra: "Nothing refuses it at startup, for that reason — unlike a repo-supplied check name."
|
|
3855
|
+
}
|
|
3856
|
+
]
|
|
3857
|
+
]);
|
|
3858
|
+
UNPRINTABLE = /[\p{Cc}\p{Cf}]/u;
|
|
3859
|
+
});
|
|
3860
|
+
|
|
3475
3861
|
// src/config.ts
|
|
3476
3862
|
var exports_config = {};
|
|
3477
3863
|
__export(exports_config, {
|
|
@@ -3483,11 +3869,11 @@ __export(exports_config, {
|
|
|
3483
3869
|
createApiClient: () => createApiClient
|
|
3484
3870
|
});
|
|
3485
3871
|
import { execSync } from "node:child_process";
|
|
3486
|
-
import { readFileSync } from "node:fs";
|
|
3872
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
3487
3873
|
import { log as log5 } from "@gethmy/harness";
|
|
3488
3874
|
import { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
|
|
3489
3875
|
import {
|
|
3490
|
-
findLocalConfigPath,
|
|
3876
|
+
findLocalConfigPath as findLocalConfigPath2,
|
|
3491
3877
|
getActiveProjectId,
|
|
3492
3878
|
getActiveWorkspaceId,
|
|
3493
3879
|
getApiKey,
|
|
@@ -3499,9 +3885,9 @@ import {
|
|
|
3499
3885
|
import { refreshOAuthToken } from "@gethmy/mcp/src/oauth-refresh.js";
|
|
3500
3886
|
function readConfigFile() {
|
|
3501
3887
|
try {
|
|
3502
|
-
return
|
|
3888
|
+
return readFileSync2(configPath(), "utf-8");
|
|
3503
3889
|
} catch {
|
|
3504
|
-
const raw =
|
|
3890
|
+
const raw = readFileSync2(legacyConfigPath(), "utf-8");
|
|
3505
3891
|
if (!warnedLegacyConfigDir) {
|
|
3506
3892
|
warnedLegacyConfigDir = true;
|
|
3507
3893
|
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.`);
|
|
@@ -3555,22 +3941,37 @@ function loadDaemonConfig() {
|
|
|
3555
3941
|
let agentName = "Harmony Agent";
|
|
3556
3942
|
let agentIdentifier2 = "harmony-daemon";
|
|
3557
3943
|
let agentColor = "#57b8a5";
|
|
3944
|
+
let allowRepoOverrides = false;
|
|
3558
3945
|
const configWarnings = [...contextWarnings];
|
|
3559
3946
|
try {
|
|
3560
3947
|
const raw = readConfigFile();
|
|
3561
3948
|
const parsed = JSON.parse(raw);
|
|
3562
3949
|
configWarnings.push(...findRemovedConfigKeys(parsed));
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
if (typeof
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3950
|
+
const block = own(parsed, "agent");
|
|
3951
|
+
if (block) {
|
|
3952
|
+
agentOverrides = block;
|
|
3953
|
+
}
|
|
3954
|
+
const rawName = own(parsed, "agentName");
|
|
3955
|
+
if (typeof rawName === "string" && rawName.trim())
|
|
3956
|
+
agentName = rawName.trim();
|
|
3957
|
+
const rawIdentifier = own(parsed, "agentIdentifier");
|
|
3958
|
+
if (typeof rawIdentifier === "string" && rawIdentifier.trim())
|
|
3959
|
+
agentIdentifier2 = rawIdentifier.trim();
|
|
3960
|
+
const rawColor = own(parsed, "agentColor");
|
|
3961
|
+
if (typeof rawColor === "string" && rawColor.trim())
|
|
3962
|
+
agentColor = rawColor.trim();
|
|
3963
|
+
if (own(parsed, "allowRepoOverrides") === true)
|
|
3964
|
+
allowRepoOverrides = true;
|
|
3572
3965
|
} catch {}
|
|
3573
|
-
const
|
|
3966
|
+
const repo = readRepoAgentConfig({
|
|
3967
|
+
repoRoot,
|
|
3968
|
+
globalConfigPath: contextSource.globalPath,
|
|
3969
|
+
allowRepoOverrides
|
|
3970
|
+
});
|
|
3971
|
+
configWarnings.push(...repo.warnings);
|
|
3972
|
+
const operatorAgent = mergeAgentConfig(DEFAULT_AGENT_CONFIG, agentOverrides);
|
|
3973
|
+
const agent = mergeAgentConfig(operatorAgent, repo.overrides);
|
|
3974
|
+
const repoArmedGates = repoArmedGatesOf(repo.suppliedPaths, operatorAgent, agent);
|
|
3574
3975
|
configWarnings.push(...findConfigTypeMismatches(DEFAULT_AGENT_CONFIG, agentOverrides));
|
|
3575
3976
|
if (agent.runner !== "cli" && agent.runner !== "sdk") {
|
|
3576
3977
|
agent.runner = DEFAULT_AGENT_CONFIG.runner;
|
|
@@ -3587,13 +3988,16 @@ function loadDaemonConfig() {
|
|
|
3587
3988
|
agentColor,
|
|
3588
3989
|
agent,
|
|
3589
3990
|
agentOverrides,
|
|
3991
|
+
repoAgentOverrides: repo.overrides,
|
|
3992
|
+
repoAgentPaths: repo.suppliedPaths,
|
|
3993
|
+
repoArmedGates,
|
|
3590
3994
|
contextSource,
|
|
3591
3995
|
configWarnings
|
|
3592
3996
|
};
|
|
3593
3997
|
}
|
|
3594
3998
|
function resolveContextSource(repoRoot) {
|
|
3595
3999
|
const globalPath = getConfigPath();
|
|
3596
|
-
const localPath =
|
|
4000
|
+
const localPath = findLocalConfigPath2(repoRoot);
|
|
3597
4001
|
if (localPath === null) {
|
|
3598
4002
|
return {
|
|
3599
4003
|
source: {
|
|
@@ -3647,6 +4051,7 @@ var init_config = __esm(() => {
|
|
|
3647
4051
|
init_config_merge();
|
|
3648
4052
|
init_config_validation();
|
|
3649
4053
|
init_paths();
|
|
4054
|
+
init_repo_agent_config();
|
|
3650
4055
|
init_types2();
|
|
3651
4056
|
});
|
|
3652
4057
|
|
|
@@ -4156,7 +4561,7 @@ import {
|
|
|
4156
4561
|
mkdirSync as mkdirSync2,
|
|
4157
4562
|
mkdtempSync,
|
|
4158
4563
|
readdirSync as readdirSync2,
|
|
4159
|
-
readFileSync as
|
|
4564
|
+
readFileSync as readFileSync3,
|
|
4160
4565
|
rmSync,
|
|
4161
4566
|
statSync as statSync2,
|
|
4162
4567
|
unlinkSync,
|
|
@@ -4233,7 +4638,7 @@ function sameBytes(a, b) {
|
|
|
4233
4638
|
return false;
|
|
4234
4639
|
if (sa.size > MAX_FILE_BYTES)
|
|
4235
4640
|
return false;
|
|
4236
|
-
return
|
|
4641
|
+
return readFileSync3(a).equals(readFileSync3(b));
|
|
4237
4642
|
} catch {
|
|
4238
4643
|
return false;
|
|
4239
4644
|
}
|
|
@@ -4257,7 +4662,7 @@ function applyChanges(args) {
|
|
|
4257
4662
|
for (const path of [...changes.modified, ...changes.added]) {
|
|
4258
4663
|
const from = join3(scratch, path);
|
|
4259
4664
|
const to = safeDestination(clean, path);
|
|
4260
|
-
const bytes =
|
|
4665
|
+
const bytes = readFileSync3(from);
|
|
4261
4666
|
if (bytes.byteLength > MAX_FILE_BYTES) {
|
|
4262
4667
|
throw new Error(`the repair wrote ${path} at ${bytes.byteLength} bytes, over the ${MAX_FILE_BYTES} cap`);
|
|
4263
4668
|
}
|
|
@@ -4314,7 +4719,8 @@ import {
|
|
|
4314
4719
|
isPrUrlOnOrigin,
|
|
4315
4720
|
log as log9,
|
|
4316
4721
|
removeWorktreeHoldingBranch,
|
|
4317
|
-
resolvePrHeadBranch
|
|
4722
|
+
resolvePrHeadBranch,
|
|
4723
|
+
shouldInstall
|
|
4318
4724
|
} from "@gethmy/harness";
|
|
4319
4725
|
function gitErrorDetail(err) {
|
|
4320
4726
|
const e = err;
|
|
@@ -4371,6 +4777,8 @@ function checkoutExistingBranch(basePath, branchName, opts = {}) {
|
|
|
4371
4777
|
} catch (err) {
|
|
4372
4778
|
throw new Error(`Failed to create review worktree for ${branchName}: ${gitErrorDetail(err)}`);
|
|
4373
4779
|
}
|
|
4780
|
+
if (!shouldInstall(worktreeDir, opts.commands))
|
|
4781
|
+
return worktreeDir;
|
|
4374
4782
|
log9.info(TAG9, "Installing dependencies in review worktree...");
|
|
4375
4783
|
try {
|
|
4376
4784
|
execSync2(installCommand(opts.ignoreScripts !== false), {
|
|
@@ -4457,7 +4865,7 @@ var init_review_worktree = __esm(() => {
|
|
|
4457
4865
|
// src/ci-patch.ts
|
|
4458
4866
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
4459
4867
|
import { randomUUID } from "node:crypto";
|
|
4460
|
-
import { readFileSync as
|
|
4868
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
4461
4869
|
import { join as join4 } from "node:path";
|
|
4462
4870
|
import {
|
|
4463
4871
|
buildCommand,
|
|
@@ -4467,6 +4875,7 @@ import {
|
|
|
4467
4875
|
GIT_NO_HOOKS as GIT_NO_HOOKS4,
|
|
4468
4876
|
HARMONY_CREDENTIAL_KEYS,
|
|
4469
4877
|
log as log10,
|
|
4878
|
+
REPO_CONFIG_FILENAMES,
|
|
4470
4879
|
runInSandbox,
|
|
4471
4880
|
SdkAgentRunner,
|
|
4472
4881
|
sandboxAvailable,
|
|
@@ -4558,7 +4967,10 @@ async function verifyInSandbox(worktree, config) {
|
|
|
4558
4967
|
return null;
|
|
4559
4968
|
}
|
|
4560
4969
|
let ran = null;
|
|
4561
|
-
for (const command of [
|
|
4970
|
+
for (const command of [
|
|
4971
|
+
buildCommand(worktree, config.commands),
|
|
4972
|
+
testCommand(worktree, config.commands)
|
|
4973
|
+
]) {
|
|
4562
4974
|
if (!command)
|
|
4563
4975
|
continue;
|
|
4564
4976
|
ran = command;
|
|
@@ -4592,7 +5004,7 @@ function commitRepair(worktree, card) {
|
|
|
4592
5004
|
}
|
|
4593
5005
|
function readGitPointer(worktree) {
|
|
4594
5006
|
try {
|
|
4595
|
-
return
|
|
5007
|
+
return readFileSync4(join4(worktree, ".git"), "utf-8");
|
|
4596
5008
|
} catch {
|
|
4597
5009
|
return null;
|
|
4598
5010
|
}
|
|
@@ -4619,7 +5031,8 @@ async function repairBranch(deps) {
|
|
|
4619
5031
|
let trees = null;
|
|
4620
5032
|
try {
|
|
4621
5033
|
worktree = checkoutExistingBranch(config.worktree.basePath, branch, {
|
|
4622
|
-
ignoreScripts: true
|
|
5034
|
+
ignoreScripts: true,
|
|
5035
|
+
commands: config.commands
|
|
4623
5036
|
});
|
|
4624
5037
|
const baseSha = remoteSha(branch, worktree);
|
|
4625
5038
|
if (!baseSha) {
|
|
@@ -4761,7 +5174,8 @@ var init_ci_patch = __esm(() => {
|
|
|
4761
5174
|
/(^|\/)\.yarnrc\.ya?ml$/i,
|
|
4762
5175
|
/(^|\/)\.tool-versions$/i,
|
|
4763
5176
|
/(^|\/)\.gitattributes$/i,
|
|
4764
|
-
/(^|\/)\.gitconfig$/i
|
|
5177
|
+
/(^|\/)\.gitconfig$/i,
|
|
5178
|
+
...REPO_CONFIG_FILENAMES.map((name) => new RegExp(`(^|/)${name.replace(/\./g, "\\.")}$`, "i"))
|
|
4765
5179
|
];
|
|
4766
5180
|
});
|
|
4767
5181
|
|
|
@@ -6477,7 +6891,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
6477
6891
|
revertWarnings: []
|
|
6478
6892
|
};
|
|
6479
6893
|
if (config.verification.enabled && config.verification.lint) {
|
|
6480
|
-
await runFormatFix(worktreePath, config.verification.timeout, workerId, verificationSandbox(config));
|
|
6894
|
+
await runFormatFix(worktreePath, config.verification.timeout, workerId, verificationSandbox(config), config.commands);
|
|
6481
6895
|
}
|
|
6482
6896
|
commitUncommittedChanges(worktreePath, card);
|
|
6483
6897
|
const hasCommits = checkHasCommits(worktreePath, config.worktree.baseBranch, runBaselineSha);
|
|
@@ -7446,7 +7860,7 @@ var init_prompt = __esm(() => {
|
|
|
7446
7860
|
});
|
|
7447
7861
|
|
|
7448
7862
|
// src/review-completion.ts
|
|
7449
|
-
import { readFileSync as
|
|
7863
|
+
import { readFileSync as readFileSync5, statSync as statSync3 } from "node:fs";
|
|
7450
7864
|
import {
|
|
7451
7865
|
cleanupWorktree as cleanupWorktree3,
|
|
7452
7866
|
createPullRequest as createPullRequest2,
|
|
@@ -7528,7 +7942,7 @@ function tailRunLog(path, bytes = RUN_LOG_TAIL_BYTES) {
|
|
|
7528
7942
|
if (size === 0)
|
|
7529
7943
|
return null;
|
|
7530
7944
|
const start = Math.max(0, size - bytes);
|
|
7531
|
-
const buf =
|
|
7945
|
+
const buf = readFileSync5(path);
|
|
7532
7946
|
return buf.subarray(start).toString("utf-8");
|
|
7533
7947
|
} catch {
|
|
7534
7948
|
return null;
|
|
@@ -8252,7 +8666,7 @@ __export(exports_state_store, {
|
|
|
8252
8666
|
import {
|
|
8253
8667
|
existsSync as existsSync3,
|
|
8254
8668
|
mkdirSync as mkdirSync4,
|
|
8255
|
-
readFileSync as
|
|
8669
|
+
readFileSync as readFileSync6,
|
|
8256
8670
|
renameSync as renameSync2,
|
|
8257
8671
|
writeFileSync as writeFileSync2
|
|
8258
8672
|
} from "node:fs";
|
|
@@ -8389,7 +8803,7 @@ class StateStore {
|
|
|
8389
8803
|
if (!existsSync3(this.path))
|
|
8390
8804
|
return emptyState();
|
|
8391
8805
|
try {
|
|
8392
|
-
const raw =
|
|
8806
|
+
const raw = readFileSync6(this.path, "utf-8");
|
|
8393
8807
|
const parsed = JSON.parse(raw);
|
|
8394
8808
|
if (parsed?.version !== SCHEMA_VERSION) {
|
|
8395
8809
|
log26.warn(TAG23, `state file has version ${parsed?.version}, expected ${SCHEMA_VERSION} — migrating (preserving card budget/attempts, dropping in-flight runs)`);
|
|
@@ -9058,6 +9472,7 @@ import {
|
|
|
9058
9472
|
buildGateCollectorRegistry,
|
|
9059
9473
|
cleanupWorktree as cleanupWorktree4,
|
|
9060
9474
|
collectGateEvidence,
|
|
9475
|
+
DEV_SERVER_READY_TIMEOUT_MS,
|
|
9061
9476
|
DevServerReadinessError,
|
|
9062
9477
|
devServerLaunch,
|
|
9063
9478
|
devServerSkipReason,
|
|
@@ -9321,7 +9736,7 @@ class ReviewWorker {
|
|
|
9321
9736
|
}
|
|
9322
9737
|
this.sessionId = reviewSession && typeof reviewSession === "object" && "id" in reviewSession ? reviewSession.id ?? null : null;
|
|
9323
9738
|
const labelPromise = addLabelByName(this.client, card, "agent", "#8b5cf6");
|
|
9324
|
-
this.worktreePath = checkoutExistingBranch(this.config.worktree.basePath, this.branchName);
|
|
9739
|
+
this.worktreePath = checkoutExistingBranch(this.config.worktree.basePath, this.branchName, { commands: this.config.commands });
|
|
9325
9740
|
await labelPromise;
|
|
9326
9741
|
}
|
|
9327
9742
|
if (this.aborted)
|
|
@@ -9844,14 +10259,15 @@ No verdict was produced, so this card has not been reviewed. The run's log is at
|
|
|
9844
10259
|
}
|
|
9845
10260
|
}
|
|
9846
10261
|
async startDevServer(card, cwd, port) {
|
|
9847
|
-
const declined = devServerSkipReason(cwd);
|
|
10262
|
+
const declined = devServerSkipReason(cwd, this.config.commands);
|
|
9848
10263
|
if (declined)
|
|
9849
10264
|
return { previewUrl: null, skipReason: declined };
|
|
9850
10265
|
log29.info(this.tag, `Starting dev server on port ${port}...`);
|
|
9851
10266
|
const launch = devServerLaunch({
|
|
9852
10267
|
worktreePath: cwd,
|
|
9853
10268
|
port,
|
|
9854
|
-
sandbox: verificationSandbox2(this.config)
|
|
10269
|
+
sandbox: verificationSandbox2(this.config),
|
|
10270
|
+
operator: this.config.commands
|
|
9855
10271
|
});
|
|
9856
10272
|
this.devServerContainer = launch.containerName ?? null;
|
|
9857
10273
|
if (launch.containerName) {
|
|
@@ -9877,13 +10293,14 @@ No verdict was produced, so this card has not been reviewed. The run's log is at
|
|
|
9877
10293
|
if (devServerSpawnError) {
|
|
9878
10294
|
throw new DevServerReadinessError(`dev server failed to start (${launch.cmd}): ${devServerSpawnError.message}`);
|
|
9879
10295
|
}
|
|
9880
|
-
await waitForDevServer(this.devServerProcess,
|
|
10296
|
+
await waitForDevServer(this.devServerProcess, DEV_SERVER_READY_TIMEOUT_MS);
|
|
9881
10297
|
await probeDevServer(port);
|
|
9882
10298
|
} catch (err) {
|
|
9883
10299
|
this.killDevServer();
|
|
10300
|
+
const failure = sanitizeCiText3(err instanceof Error ? err.message : String(err), REVIEW_FAILURE_REASON_MAX);
|
|
9884
10301
|
return {
|
|
9885
10302
|
previewUrl: null,
|
|
9886
|
-
skipReason:
|
|
10303
|
+
skipReason: launch.origin ? `${launch.origin}, and ${failure}` : failure
|
|
9887
10304
|
};
|
|
9888
10305
|
}
|
|
9889
10306
|
log29.info(this.tag, `Dev server ready on port ${port}`);
|
|
@@ -11256,7 +11673,7 @@ function workspaceConfigToPartial(ws) {
|
|
|
11256
11673
|
}
|
|
11257
11674
|
function resolveEffectiveAgentConfig(cfg, ws) {
|
|
11258
11675
|
const withWorkspace = ws ? mergeAgentConfig(DEFAULT_AGENT_CONFIG, workspaceConfigToPartial(ws)) : DEFAULT_AGENT_CONFIG;
|
|
11259
|
-
return mergeAgentConfig(withWorkspace, cfg.agentOverrides);
|
|
11676
|
+
return mergeAgentConfig(mergeAgentConfig(withWorkspace, cfg.agentOverrides), cfg.repoAgentOverrides);
|
|
11260
11677
|
}
|
|
11261
11678
|
var init_workspace_model_config = __esm(() => {
|
|
11262
11679
|
init_config_merge();
|
|
@@ -11749,7 +12166,8 @@ class Worker {
|
|
|
11749
12166
|
}
|
|
11750
12167
|
this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName, {
|
|
11751
12168
|
continueExisting: target.continueExisting,
|
|
11752
|
-
branchExistsOnOrigin: target.existsOnOrigin
|
|
12169
|
+
branchExistsOnOrigin: target.existsOnOrigin,
|
|
12170
|
+
commands: this.config.commands
|
|
11753
12171
|
});
|
|
11754
12172
|
this.runBaselineSha = readWorktreeHead(this.worktreePath);
|
|
11755
12173
|
}
|
|
@@ -14423,7 +14841,7 @@ __export(exports_port_registry, {
|
|
|
14423
14841
|
import {
|
|
14424
14842
|
existsSync as existsSync4,
|
|
14425
14843
|
mkdirSync as mkdirSync5,
|
|
14426
|
-
readFileSync as
|
|
14844
|
+
readFileSync as readFileSync7,
|
|
14427
14845
|
renameSync as renameSync3,
|
|
14428
14846
|
writeFileSync as writeFileSync4
|
|
14429
14847
|
} from "node:fs";
|
|
@@ -14436,7 +14854,7 @@ function load(path) {
|
|
|
14436
14854
|
if (!existsSync4(path))
|
|
14437
14855
|
return {};
|
|
14438
14856
|
try {
|
|
14439
|
-
const raw =
|
|
14857
|
+
const raw = readFileSync7(path, "utf-8");
|
|
14440
14858
|
const parsed = JSON.parse(raw);
|
|
14441
14859
|
if (parsed && typeof parsed === "object")
|
|
14442
14860
|
return parsed;
|
|
@@ -15888,7 +16306,11 @@ function contextSourceLine(config) {
|
|
|
15888
16306
|
if (unreadablePin !== null) {
|
|
15889
16307
|
return `all of it ${globalPath} (global) — ${unreadablePin} is present but UNREADABLE, so its pin was ignored`;
|
|
15890
16308
|
}
|
|
15891
|
-
|
|
16309
|
+
if (scope !== "repo") {
|
|
16310
|
+
return `all of it ${globalPath} (global; no repo pin found)`;
|
|
16311
|
+
}
|
|
16312
|
+
const pinLabel = config.repoAgentPaths.size > 0 ? "repo pin, +auto-merge" : "repo pin";
|
|
16313
|
+
return `project+workspace ${path} (${pinLabel}) · rest ${globalPath}`;
|
|
15892
16314
|
}
|
|
15893
16315
|
function titleRule(title) {
|
|
15894
16316
|
const prefix = "─── ";
|
|
@@ -15929,7 +16351,7 @@ import {
|
|
|
15929
16351
|
copyFileSync,
|
|
15930
16352
|
existsSync as existsSync5,
|
|
15931
16353
|
mkdirSync as mkdirSync6,
|
|
15932
|
-
readFileSync as
|
|
16354
|
+
readFileSync as readFileSync8,
|
|
15933
16355
|
renameSync as renameSync4,
|
|
15934
16356
|
writeFileSync as writeFileSync5
|
|
15935
16357
|
} from "node:fs";
|
|
@@ -15951,7 +16373,7 @@ function atomicWriteJson(path, value) {
|
|
|
15951
16373
|
}
|
|
15952
16374
|
function readTargetFile(path) {
|
|
15953
16375
|
try {
|
|
15954
|
-
const parsed = JSON.parse(
|
|
16376
|
+
const parsed = JSON.parse(readFileSync8(path, "utf-8"));
|
|
15955
16377
|
if (!parsed || typeof parsed !== "object")
|
|
15956
16378
|
return null;
|
|
15957
16379
|
return parsed;
|
|
@@ -16005,7 +16427,7 @@ function migrateLegacyStateFile(opts) {
|
|
|
16005
16427
|
return NO_MIGRATION;
|
|
16006
16428
|
let legacy;
|
|
16007
16429
|
try {
|
|
16008
|
-
const parsed = JSON.parse(
|
|
16430
|
+
const parsed = JSON.parse(readFileSync8(legacyPath, "utf-8"));
|
|
16009
16431
|
if (!parsed || typeof parsed !== "object")
|
|
16010
16432
|
throw new Error("not an object");
|
|
16011
16433
|
legacy = parsed;
|
|
@@ -16534,7 +16956,7 @@ __export(exports_run_stats, {
|
|
|
16534
16956
|
computeRunStats: () => computeRunStats,
|
|
16535
16957
|
buildRunListRows: () => buildRunListRows
|
|
16536
16958
|
});
|
|
16537
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
16959
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync4 } from "node:fs";
|
|
16538
16960
|
import { join as join8 } from "node:path";
|
|
16539
16961
|
function runsDir2(base) {
|
|
16540
16962
|
return runsDir(base);
|
|
@@ -16617,7 +17039,7 @@ function tailText(path, bytes = 4096) {
|
|
|
16617
17039
|
const size = statSync4(path).size;
|
|
16618
17040
|
if (size === 0)
|
|
16619
17041
|
return "";
|
|
16620
|
-
const buf =
|
|
17042
|
+
const buf = readFileSync9(path);
|
|
16621
17043
|
const start = Math.max(0, size - bytes);
|
|
16622
17044
|
let text = buf.subarray(start).toString("utf-8");
|
|
16623
17045
|
if (start > 0) {
|
|
@@ -16639,7 +17061,7 @@ function grepRunLogs(files, pattern, opts = {}) {
|
|
|
16639
17061
|
continue;
|
|
16640
17062
|
let content;
|
|
16641
17063
|
try {
|
|
16642
|
-
content =
|
|
17064
|
+
content = readFileSync9(file.path, "utf-8");
|
|
16643
17065
|
} catch {
|
|
16644
17066
|
continue;
|
|
16645
17067
|
}
|
|
@@ -17239,7 +17661,9 @@ async function main() {
|
|
|
17239
17661
|
throw err;
|
|
17240
17662
|
}
|
|
17241
17663
|
try {
|
|
17242
|
-
validateAutoMergeConfig(config.agent)
|
|
17664
|
+
for (const warning of validateAutoMergeConfig(config.agent, config.repoAgentPaths, config.repoArmedGates)) {
|
|
17665
|
+
banner.warn(warning);
|
|
17666
|
+
}
|
|
17243
17667
|
validateBudgetConfig(config.agent);
|
|
17244
17668
|
validateSweepConfig(config.agent);
|
|
17245
17669
|
validateRankingConfig(config.agent);
|