@gethmy/agent 1.35.0 → 1.36.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 +6 -1
- package/dist/cli.js +363 -207
- package/dist/index.js +357 -203
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -821,16 +821,121 @@ function buildGaveUpComment(maxAttempts, failures, pauseEnabled) {
|
|
|
821
821
|
`);
|
|
822
822
|
}
|
|
823
823
|
|
|
824
|
+
// src/config-merge.ts
|
|
825
|
+
function mergeAgentConfig(defaults, overrides) {
|
|
826
|
+
return mergeObjects(defaults, overrides);
|
|
827
|
+
}
|
|
828
|
+
function isPlainObject(value) {
|
|
829
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
const proto = Object.getPrototypeOf(value);
|
|
833
|
+
return proto === Object.prototype || proto === null;
|
|
834
|
+
}
|
|
835
|
+
function mergeObjects(base, override) {
|
|
836
|
+
const result = {};
|
|
837
|
+
for (const key of Object.keys(base)) {
|
|
838
|
+
if (FORBIDDEN_KEYS.has(key))
|
|
839
|
+
continue;
|
|
840
|
+
result[key] = mergeValue(base[key], override[key]);
|
|
841
|
+
}
|
|
842
|
+
for (const key of Object.keys(override)) {
|
|
843
|
+
if (FORBIDDEN_KEYS.has(key))
|
|
844
|
+
continue;
|
|
845
|
+
if (Object.hasOwn(base, key))
|
|
846
|
+
continue;
|
|
847
|
+
if (override[key] === undefined)
|
|
848
|
+
continue;
|
|
849
|
+
result[key] = copy(override[key]);
|
|
850
|
+
}
|
|
851
|
+
return result;
|
|
852
|
+
}
|
|
853
|
+
function mergeValue(base, override) {
|
|
854
|
+
if (override === undefined)
|
|
855
|
+
return copy(base);
|
|
856
|
+
if (isPlainObject(base)) {
|
|
857
|
+
return isPlainObject(override) ? mergeObjects(base, override) : copy(base);
|
|
858
|
+
}
|
|
859
|
+
if (Array.isArray(base)) {
|
|
860
|
+
return Array.isArray(override) ? override.map(copy) : copy(base);
|
|
861
|
+
}
|
|
862
|
+
if (base !== undefined && (isPlainObject(override) || Array.isArray(override))) {
|
|
863
|
+
return copy(base);
|
|
864
|
+
}
|
|
865
|
+
if (Array.isArray(override))
|
|
866
|
+
return override.map(copy);
|
|
867
|
+
return override;
|
|
868
|
+
}
|
|
869
|
+
function findConfigTypeMismatches(defaults, overrides) {
|
|
870
|
+
const found = [];
|
|
871
|
+
walk(defaults, overrides, "agent", found);
|
|
872
|
+
return found;
|
|
873
|
+
}
|
|
874
|
+
function walk(base, override, path, found) {
|
|
875
|
+
for (const key of Object.keys(base)) {
|
|
876
|
+
if (FORBIDDEN_KEYS.has(key))
|
|
877
|
+
continue;
|
|
878
|
+
const baseValue = base[key];
|
|
879
|
+
const overrideValue = override[key];
|
|
880
|
+
if (overrideValue === undefined)
|
|
881
|
+
continue;
|
|
882
|
+
const here = `${path}.${key}`;
|
|
883
|
+
if (isPlainObject(baseValue)) {
|
|
884
|
+
if (isPlainObject(overrideValue)) {
|
|
885
|
+
walk(baseValue, overrideValue, here, found);
|
|
886
|
+
} else {
|
|
887
|
+
found.push(mismatch(here, overrideValue, "a block of settings"));
|
|
888
|
+
}
|
|
889
|
+
} else if (Array.isArray(baseValue) && !Array.isArray(overrideValue)) {
|
|
890
|
+
found.push(mismatch(here, overrideValue, "a list"));
|
|
891
|
+
} else if (baseValue !== undefined && !isPlainObject(baseValue) && !Array.isArray(baseValue) && (isPlainObject(overrideValue) || Array.isArray(overrideValue))) {
|
|
892
|
+
found.push(mismatch(here, overrideValue, "a single value"));
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
function mismatch(path, value, expected) {
|
|
897
|
+
return `${path} is ${describe(value)} in your config but ${expected}, so it was IGNORED and the default used. Fix the type or remove the key.`;
|
|
898
|
+
}
|
|
899
|
+
function describe(value) {
|
|
900
|
+
if (value === null)
|
|
901
|
+
return "null";
|
|
902
|
+
if (Array.isArray(value))
|
|
903
|
+
return "a list";
|
|
904
|
+
if (isPlainObject(value))
|
|
905
|
+
return "a block of settings";
|
|
906
|
+
return `a ${typeof value}`;
|
|
907
|
+
}
|
|
908
|
+
function copy(value) {
|
|
909
|
+
if (Array.isArray(value))
|
|
910
|
+
return value.map(copy);
|
|
911
|
+
if (isPlainObject(value)) {
|
|
912
|
+
const out = {};
|
|
913
|
+
for (const key of Object.keys(value)) {
|
|
914
|
+
if (FORBIDDEN_KEYS.has(key))
|
|
915
|
+
continue;
|
|
916
|
+
out[key] = copy(value[key]);
|
|
917
|
+
}
|
|
918
|
+
return out;
|
|
919
|
+
}
|
|
920
|
+
return value;
|
|
921
|
+
}
|
|
922
|
+
var FORBIDDEN_KEYS;
|
|
923
|
+
var init_config_merge = __esm(() => {
|
|
924
|
+
FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
925
|
+
});
|
|
926
|
+
|
|
824
927
|
// src/config-validation.ts
|
|
825
928
|
var exports_config_validation = {};
|
|
826
929
|
__export(exports_config_validation, {
|
|
827
930
|
validateVerificationConfig: () => validateVerificationConfig,
|
|
828
931
|
validateSweepConfig: () => validateSweepConfig,
|
|
829
932
|
validateRankingConfig: () => validateRankingConfig,
|
|
933
|
+
validateHttpConfig: () => validateHttpConfig,
|
|
830
934
|
validateColumnReferences: () => validateColumnReferences,
|
|
831
935
|
validateBudgetConfig: () => validateBudgetConfig,
|
|
832
936
|
validateAutoMergeConfig: () => validateAutoMergeConfig,
|
|
833
937
|
validateAndListColumns: () => validateAndListColumns,
|
|
938
|
+
isLoopbackBindAddr: () => isLoopbackBindAddr,
|
|
834
939
|
findRemovedConfigKeys: () => findRemovedConfigKeys,
|
|
835
940
|
REMOVED_CONFIG_KEYS: () => REMOVED_CONFIG_KEYS,
|
|
836
941
|
ConfigValidationError: () => ConfigValidationError
|
|
@@ -946,6 +1051,31 @@ function validateAutoMergeConfig(config) {
|
|
|
946
1051
|
throw new ConfigValidationError(`Invalid agent config — ${issues.join("; ")}`, issues);
|
|
947
1052
|
}
|
|
948
1053
|
}
|
|
1054
|
+
function isLoopbackBindAddr(addr) {
|
|
1055
|
+
const value = addr.toLowerCase();
|
|
1056
|
+
if (value === "localhost" || value === "::1")
|
|
1057
|
+
return true;
|
|
1058
|
+
const v4 = value.startsWith("::ffff:") ? value.slice("::ffff:".length) : value;
|
|
1059
|
+
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(v4);
|
|
1060
|
+
if (!match)
|
|
1061
|
+
return false;
|
|
1062
|
+
const octets = match.slice(1).map(Number);
|
|
1063
|
+
if (octets.some((octet) => octet > 255))
|
|
1064
|
+
return false;
|
|
1065
|
+
return octets[0] === 127;
|
|
1066
|
+
}
|
|
1067
|
+
function validateHttpConfig(config) {
|
|
1068
|
+
const bindAddr = config.http.bindAddr;
|
|
1069
|
+
const issues = [];
|
|
1070
|
+
if (typeof bindAddr !== "string" || !isLoopbackBindAddr(bindAddr)) {
|
|
1071
|
+
issues.push(`http.bindAddr: ${JSON.stringify(bindAddr)} is not a loopback address — the control server is UNAUTHENTICATED (no token, no Origin or Host check), so binding it anywhere else publishes the daemon's kill switch (\`POST /sweep/stop\`), its per-card commands (\`POST /stop/:cardId\`) and a \`GET /status\` carrying card ids, branch names and spend to every host that can reach that interface. Use 127.0.0.1 — any 127.0.0.0/8 address, ::1 or localhost also work. To reach the daemon from another machine, tunnel to loopback (\`ssh -L ${config.http.port}:127.0.0.1:${config.http.port} <host>\`); there is no shared-secret mode, by design (#1061).`);
|
|
1072
|
+
}
|
|
1073
|
+
if (issues.length > 0) {
|
|
1074
|
+
throw new ConfigValidationError(`Invalid agent config — HTTP control server:
|
|
1075
|
+
- ${issues.join(`
|
|
1076
|
+
- `)}`, issues);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
949
1079
|
function validateSweepConfig(config) {
|
|
950
1080
|
const sweep = config.sweep;
|
|
951
1081
|
const issues = [];
|
|
@@ -959,7 +1089,7 @@ function validateSweepConfig(config) {
|
|
|
959
1089
|
issues.push("sweep.enabled: true with no ceiling at all — sweep.maxCardsPerSweep and budget.dailyBudgetCents are both opted out (-1). A self-claiming daemon needs at least one: set a card cap (e.g. 10) or a daily spend cap (e.g. 40000 for $400.00/day).");
|
|
960
1090
|
}
|
|
961
1091
|
if (sweep.enabled && !config.http.enabled) {
|
|
962
|
-
issues.push("sweep.enabled: true but http.enabled is false — the sweep kill switch IS the local HTTP server (`POST /sweep/stop`, `harmony-agent sweep stop`), so this configuration ships a self-claiming daemon
|
|
1092
|
+
issues.push("sweep.enabled: true but http.enabled is false — the local sweep kill switch IS the local HTTP server (`POST /sweep/stop`, `harmony-agent sweep stop`), so this configuration ships a self-claiming daemon without it. The board can stop and resume the sweep too (#1060), but only while this daemon still reaches the API and realtime — and that is exactly what is broken when you most need a stop. Set http.enabled: true.");
|
|
963
1093
|
}
|
|
964
1094
|
if (sweep.enabled && config.pickupColumns.length === 0) {
|
|
965
1095
|
issues.push("sweep.enabled: true but pickupColumns is empty — the sweep has no column to claim from");
|
|
@@ -1462,9 +1592,9 @@ function isGateOperator(value) {
|
|
|
1462
1592
|
return typeof value === "string" && GATE_OPERATORS.includes(value);
|
|
1463
1593
|
}
|
|
1464
1594
|
function gateEvaluate(gateSpec, evidence) {
|
|
1465
|
-
const structured =
|
|
1466
|
-
const safeStructured =
|
|
1467
|
-
if (!
|
|
1595
|
+
const structured = isPlainObject2(evidence) ? evidence.structured ?? {} : {};
|
|
1596
|
+
const safeStructured = isPlainObject2(structured) ? structured : {};
|
|
1597
|
+
if (!isPlainObject2(gateSpec)) {
|
|
1468
1598
|
return {
|
|
1469
1599
|
passed: false,
|
|
1470
1600
|
findings: [{ level: "error", message: "Malformed gate: not an object." }],
|
|
@@ -1496,7 +1626,7 @@ function gateEvaluate(gateSpec, evidence) {
|
|
|
1496
1626
|
structured: safeStructured
|
|
1497
1627
|
};
|
|
1498
1628
|
}
|
|
1499
|
-
if (!
|
|
1629
|
+
if (!isPlainObject2(evidence)) {
|
|
1500
1630
|
return {
|
|
1501
1631
|
passed: false,
|
|
1502
1632
|
findings: [
|
|
@@ -1569,7 +1699,7 @@ function gateEvaluate(gateSpec, evidence) {
|
|
|
1569
1699
|
return { passed: predicatePassed, findings, structured: safeStructured };
|
|
1570
1700
|
}
|
|
1571
1701
|
function evaluateCondition(raw, structured) {
|
|
1572
|
-
if (!
|
|
1702
|
+
if (!isPlainObject2(raw)) {
|
|
1573
1703
|
return {
|
|
1574
1704
|
ok: false,
|
|
1575
1705
|
finding: {
|
|
@@ -1647,7 +1777,7 @@ function evaluateCondition(raw, structured) {
|
|
|
1647
1777
|
}
|
|
1648
1778
|
};
|
|
1649
1779
|
}
|
|
1650
|
-
function
|
|
1780
|
+
function isPlainObject2(value) {
|
|
1651
1781
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1652
1782
|
}
|
|
1653
1783
|
function blockedDetail(structured) {
|
|
@@ -3019,97 +3149,8 @@ function loadDaemonConfig() {
|
|
|
3019
3149
|
if (typeof parsed.agentColor === "string" && parsed.agentColor.trim())
|
|
3020
3150
|
agentColor = parsed.agentColor.trim();
|
|
3021
3151
|
} catch {}
|
|
3022
|
-
const agent =
|
|
3023
|
-
|
|
3024
|
-
...agentOverrides,
|
|
3025
|
-
completion: {
|
|
3026
|
-
...DEFAULT_AGENT_CONFIG.completion,
|
|
3027
|
-
...agentOverrides.completion ?? {}
|
|
3028
|
-
},
|
|
3029
|
-
ranking: {
|
|
3030
|
-
...DEFAULT_AGENT_CONFIG.ranking,
|
|
3031
|
-
...agentOverrides.ranking ?? {}
|
|
3032
|
-
},
|
|
3033
|
-
claude: {
|
|
3034
|
-
...DEFAULT_AGENT_CONFIG.claude,
|
|
3035
|
-
...agentOverrides.claude ?? {}
|
|
3036
|
-
},
|
|
3037
|
-
worktree: {
|
|
3038
|
-
...DEFAULT_AGENT_CONFIG.worktree,
|
|
3039
|
-
...agentOverrides.worktree ?? {}
|
|
3040
|
-
},
|
|
3041
|
-
verification: {
|
|
3042
|
-
...DEFAULT_AGENT_CONFIG.verification,
|
|
3043
|
-
...agentOverrides.verification ?? {}
|
|
3044
|
-
},
|
|
3045
|
-
review: {
|
|
3046
|
-
...DEFAULT_AGENT_CONFIG.review,
|
|
3047
|
-
...agentOverrides.review ?? {},
|
|
3048
|
-
autoMerge: {
|
|
3049
|
-
...DEFAULT_AGENT_CONFIG.review.autoMerge,
|
|
3050
|
-
...agentOverrides.review?.autoMerge ?? {},
|
|
3051
|
-
ciRepair: {
|
|
3052
|
-
...DEFAULT_AGENT_CONFIG.review.autoMerge.ciRepair,
|
|
3053
|
-
...agentOverrides.review?.autoMerge?.ciRepair ?? {},
|
|
3054
|
-
patch: {
|
|
3055
|
-
...DEFAULT_AGENT_CONFIG.review.autoMerge.ciRepair.patch,
|
|
3056
|
-
...agentOverrides.review?.autoMerge?.ciRepair?.patch ?? {}
|
|
3057
|
-
}
|
|
3058
|
-
},
|
|
3059
|
-
independentReview: {
|
|
3060
|
-
...DEFAULT_AGENT_CONFIG.review.autoMerge.independentReview,
|
|
3061
|
-
...agentOverrides.review?.autoMerge?.independentReview ?? {},
|
|
3062
|
-
verdict: {
|
|
3063
|
-
...DEFAULT_AGENT_CONFIG.review.autoMerge.independentReview.verdict,
|
|
3064
|
-
...agentOverrides.review?.autoMerge?.independentReview?.verdict ?? {}
|
|
3065
|
-
}
|
|
3066
|
-
}
|
|
3067
|
-
}
|
|
3068
|
-
},
|
|
3069
|
-
budget: {
|
|
3070
|
-
...DEFAULT_AGENT_CONFIG.budget,
|
|
3071
|
-
...agentOverrides.budget ?? {},
|
|
3072
|
-
pause: {
|
|
3073
|
-
...DEFAULT_AGENT_CONFIG.budget.pause,
|
|
3074
|
-
...agentOverrides.budget?.pause ?? {}
|
|
3075
|
-
}
|
|
3076
|
-
},
|
|
3077
|
-
sdk: {
|
|
3078
|
-
...DEFAULT_AGENT_CONFIG.sdk,
|
|
3079
|
-
...agentOverrides.sdk ?? {}
|
|
3080
|
-
},
|
|
3081
|
-
http: {
|
|
3082
|
-
...DEFAULT_AGENT_CONFIG.http,
|
|
3083
|
-
...agentOverrides.http ?? {}
|
|
3084
|
-
},
|
|
3085
|
-
timing: {
|
|
3086
|
-
...DEFAULT_AGENT_CONFIG.timing,
|
|
3087
|
-
...agentOverrides.timing ?? {}
|
|
3088
|
-
},
|
|
3089
|
-
planning: {
|
|
3090
|
-
...DEFAULT_AGENT_CONFIG.planning,
|
|
3091
|
-
...agentOverrides.planning ?? {}
|
|
3092
|
-
},
|
|
3093
|
-
playbooks: {
|
|
3094
|
-
...DEFAULT_AGENT_CONFIG.playbooks,
|
|
3095
|
-
...agentOverrides.playbooks ?? {}
|
|
3096
|
-
},
|
|
3097
|
-
contractFirst: {
|
|
3098
|
-
...DEFAULT_AGENT_CONFIG.contractFirst,
|
|
3099
|
-
...agentOverrides.contractFirst ?? {}
|
|
3100
|
-
},
|
|
3101
|
-
boardReview: {
|
|
3102
|
-
...DEFAULT_AGENT_CONFIG.boardReview,
|
|
3103
|
-
...agentOverrides.boardReview ?? {}
|
|
3104
|
-
},
|
|
3105
|
-
sweep: {
|
|
3106
|
-
...DEFAULT_AGENT_CONFIG.sweep,
|
|
3107
|
-
...agentOverrides.sweep ?? {},
|
|
3108
|
-
trustedAuthors: [
|
|
3109
|
-
...agentOverrides.sweep?.trustedAuthors ?? DEFAULT_AGENT_CONFIG.sweep.trustedAuthors
|
|
3110
|
-
]
|
|
3111
|
-
}
|
|
3112
|
-
};
|
|
3152
|
+
const agent = mergeAgentConfig(DEFAULT_AGENT_CONFIG, agentOverrides);
|
|
3153
|
+
configWarnings.push(...findConfigTypeMismatches(DEFAULT_AGENT_CONFIG, agentOverrides));
|
|
3113
3154
|
if (agent.runner !== "cli" && agent.runner !== "sdk") {
|
|
3114
3155
|
agent.runner = DEFAULT_AGENT_CONFIG.runner;
|
|
3115
3156
|
}
|
|
@@ -3181,6 +3222,7 @@ function createApiClient(config) {
|
|
|
3181
3222
|
}
|
|
3182
3223
|
var NOT_A_REPOSITORY_EXIT = 128;
|
|
3183
3224
|
var init_config = __esm(() => {
|
|
3225
|
+
init_config_merge();
|
|
3184
3226
|
init_config_validation();
|
|
3185
3227
|
init_types2();
|
|
3186
3228
|
});
|
|
@@ -3211,6 +3253,9 @@ class HttpServer {
|
|
|
3211
3253
|
return this.boundPort;
|
|
3212
3254
|
}
|
|
3213
3255
|
async start() {
|
|
3256
|
+
if (!isLoopbackBindAddr(this.opts.bindAddr)) {
|
|
3257
|
+
throw new Error(`refusing to bind the unauthenticated control server to ${JSON.stringify(this.opts.bindAddr)} — loopback only (127.0.0.0/8, ::1, localhost), see validateHttpConfig (#1061)`);
|
|
3258
|
+
}
|
|
3214
3259
|
this.server = createServer((req, res) => {
|
|
3215
3260
|
this.route(req, res).catch((err) => {
|
|
3216
3261
|
log5.error(TAG5, `unhandled: ${err instanceof Error ? err.message : err}`);
|
|
@@ -3423,7 +3468,9 @@ function busy(w) {
|
|
|
3423
3468
|
return w.cardId !== null;
|
|
3424
3469
|
}
|
|
3425
3470
|
var TAG5 = "http";
|
|
3426
|
-
var init_http_server = () => {
|
|
3471
|
+
var init_http_server = __esm(() => {
|
|
3472
|
+
init_config_validation();
|
|
3473
|
+
});
|
|
3427
3474
|
|
|
3428
3475
|
// src/identity.ts
|
|
3429
3476
|
function resolveDaemonUserId(userEmail, members, apiKeyUserId) {
|
|
@@ -3487,13 +3534,13 @@ function extractScratchTrees(cleanWorktree, commitish) {
|
|
|
3487
3534
|
function changedPaths(changes) {
|
|
3488
3535
|
return [...changes.modified, ...changes.added, ...changes.deleted];
|
|
3489
3536
|
}
|
|
3490
|
-
function
|
|
3537
|
+
function walk2(root, dir = root, out = []) {
|
|
3491
3538
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
3492
3539
|
const full = join2(dir, entry.name);
|
|
3493
3540
|
if (entry.isSymbolicLink())
|
|
3494
3541
|
continue;
|
|
3495
3542
|
if (entry.isDirectory()) {
|
|
3496
|
-
|
|
3543
|
+
walk2(root, full, out);
|
|
3497
3544
|
} else if (entry.isFile()) {
|
|
3498
3545
|
out.push(relative(root, full).split(sep).join("/"));
|
|
3499
3546
|
}
|
|
@@ -3501,8 +3548,8 @@ function walk(root, dir = root, out = []) {
|
|
|
3501
3548
|
return out;
|
|
3502
3549
|
}
|
|
3503
3550
|
function diffTrees(pristine, scratch) {
|
|
3504
|
-
const before = new Set(
|
|
3505
|
-
const after =
|
|
3551
|
+
const before = new Set(walk2(pristine));
|
|
3552
|
+
const after = walk2(scratch);
|
|
3506
3553
|
const afterSet = new Set(after);
|
|
3507
3554
|
const modified = [];
|
|
3508
3555
|
const added = [];
|
|
@@ -4916,7 +4963,7 @@ async function settlePlanCriteria(client, tag, card) {
|
|
|
4916
4963
|
if (divergent.length > 0) {
|
|
4917
4964
|
log13.warn(tag, describeDivergence(card.short_id, card.plan_id, divergent));
|
|
4918
4965
|
}
|
|
4919
|
-
const settleGroup = async (criteria, planIdOf,
|
|
4966
|
+
const settleGroup = async (criteria, planIdOf, describe2) => {
|
|
4920
4967
|
if (criteria.length === 0)
|
|
4921
4968
|
return;
|
|
4922
4969
|
try {
|
|
@@ -4925,7 +4972,7 @@ async function settlePlanCriteria(client, tag, card) {
|
|
|
4925
4972
|
status: CRITERION_COMPLETED
|
|
4926
4973
|
});
|
|
4927
4974
|
}
|
|
4928
|
-
log13.info(tag,
|
|
4975
|
+
log13.info(tag, describe2());
|
|
4929
4976
|
} catch (err) {
|
|
4930
4977
|
log13.warn(tag, `plan criteria write-back failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
4931
4978
|
}
|
|
@@ -13115,6 +13162,7 @@ __export(exports_port_registry, {
|
|
|
13115
13162
|
recordDaemonPort: () => recordDaemonPort,
|
|
13116
13163
|
lookupDaemonPort: () => lookupDaemonPort,
|
|
13117
13164
|
defaultRegistryPath: () => defaultRegistryPath,
|
|
13165
|
+
daemonBaseUrl: () => daemonBaseUrl,
|
|
13118
13166
|
clearDaemonPort: () => clearDaemonPort
|
|
13119
13167
|
});
|
|
13120
13168
|
import {
|
|
@@ -13161,6 +13209,10 @@ function recordDaemonPort(projectId, entry, path = defaultRegistryPath()) {
|
|
|
13161
13209
|
log33.warn(TAG30, `failed to record port for ${projectId}: ${err instanceof Error ? err.message : err}`);
|
|
13162
13210
|
}
|
|
13163
13211
|
}
|
|
13212
|
+
function daemonBaseUrl(bindAddr, port) {
|
|
13213
|
+
const host = bindAddr.includes(":") ? `[${bindAddr}]` : bindAddr;
|
|
13214
|
+
return `http://${host}:${port}`;
|
|
13215
|
+
}
|
|
13164
13216
|
function lookupDaemonPort(projectId, path = defaultRegistryPath()) {
|
|
13165
13217
|
const registry = load(path);
|
|
13166
13218
|
return registry[projectId] ?? null;
|
|
@@ -13845,8 +13897,63 @@ var init_sweep = __esm(() => {
|
|
|
13845
13897
|
init_unblock();
|
|
13846
13898
|
});
|
|
13847
13899
|
|
|
13848
|
-
// src/sweep-
|
|
13900
|
+
// src/sweep-commands.ts
|
|
13849
13901
|
import { log as log38 } from "@gethmy/harness";
|
|
13902
|
+
function parseSweepCommands(payload) {
|
|
13903
|
+
if (!payload || typeof payload !== "object")
|
|
13904
|
+
return [];
|
|
13905
|
+
const rows = payload.commands;
|
|
13906
|
+
if (!Array.isArray(rows))
|
|
13907
|
+
return [];
|
|
13908
|
+
const parsed = [];
|
|
13909
|
+
for (const row of rows) {
|
|
13910
|
+
if (!row || typeof row !== "object")
|
|
13911
|
+
continue;
|
|
13912
|
+
const { id, verb, createdAt } = row;
|
|
13913
|
+
if (typeof id !== "string" || !id)
|
|
13914
|
+
continue;
|
|
13915
|
+
if (verb !== "stop" && verb !== "resume")
|
|
13916
|
+
continue;
|
|
13917
|
+
parsed.push({
|
|
13918
|
+
id,
|
|
13919
|
+
verb,
|
|
13920
|
+
createdAt: typeof createdAt === "string" ? createdAt : ""
|
|
13921
|
+
});
|
|
13922
|
+
}
|
|
13923
|
+
return parsed.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
13924
|
+
}
|
|
13925
|
+
async function claimSweepCommands(client, agentId, projectId, daemonId) {
|
|
13926
|
+
const request = client.request?.bind(client);
|
|
13927
|
+
if (typeof request !== "function") {
|
|
13928
|
+
log38.warn(TAG35, "installed @gethmy/mcp has no generic request method — board sweep commands cannot be claimed; use `harmony-agent sweep stop|resume` until it is upgraded");
|
|
13929
|
+
return [];
|
|
13930
|
+
}
|
|
13931
|
+
try {
|
|
13932
|
+
const res = await request("POST", `/agents/${encodeURIComponent(agentId)}/sweep-commands/claim`, { projectId, daemonId });
|
|
13933
|
+
return parseSweepCommands(res);
|
|
13934
|
+
} catch (err) {
|
|
13935
|
+
log38.warn(TAG35, `could not claim sweep commands: ${err instanceof Error ? err.message : err}`);
|
|
13936
|
+
return [];
|
|
13937
|
+
}
|
|
13938
|
+
}
|
|
13939
|
+
async function applySweepCommands(commands, actions) {
|
|
13940
|
+
let applied = null;
|
|
13941
|
+
for (const command of commands) {
|
|
13942
|
+
if (command.verb === "stop") {
|
|
13943
|
+
await actions.halt();
|
|
13944
|
+
} else {
|
|
13945
|
+
await actions.resume();
|
|
13946
|
+
}
|
|
13947
|
+
applied = command.verb;
|
|
13948
|
+
log38.info(TAG35, `applied "${command.verb}" from the board (${command.id})`);
|
|
13949
|
+
}
|
|
13950
|
+
return applied;
|
|
13951
|
+
}
|
|
13952
|
+
var TAG35 = "sweep-cmd";
|
|
13953
|
+
var init_sweep_commands = () => {};
|
|
13954
|
+
|
|
13955
|
+
// src/sweep-guard.ts
|
|
13956
|
+
import { log as log39 } from "@gethmy/harness";
|
|
13850
13957
|
|
|
13851
13958
|
class SweepGuard {
|
|
13852
13959
|
config;
|
|
@@ -13902,7 +14009,7 @@ class SweepGuard {
|
|
|
13902
14009
|
await this.store.haltSweep(reason);
|
|
13903
14010
|
if (!already) {
|
|
13904
14011
|
const claimed = this.store.getSweep().claimed;
|
|
13905
|
-
|
|
14012
|
+
log39.info(TAG36, `claiming stopped (${reason}) after ${claimed} claimed`);
|
|
13906
14013
|
}
|
|
13907
14014
|
return this.snapshot();
|
|
13908
14015
|
}
|
|
@@ -13911,7 +14018,7 @@ class SweepGuard {
|
|
|
13911
14018
|
const was = before.haltReason ? this.describeStop(before.haltReason) : null;
|
|
13912
14019
|
await this.store.resumeSweep();
|
|
13913
14020
|
if (was)
|
|
13914
|
-
|
|
14021
|
+
log39.info(TAG36, `claiming resumed by the operator — was stopped: ${was}`);
|
|
13915
14022
|
return this.snapshot();
|
|
13916
14023
|
}
|
|
13917
14024
|
snapshot() {
|
|
@@ -13973,11 +14080,11 @@ function sweepBannerLine(config) {
|
|
|
13973
14080
|
const authors = extra ? `you + ${extra} trusted author${extra === 1 ? "" : "s"}` : "you only";
|
|
13974
14081
|
return `Sweep ON — claims ${scope} in ${config.pickupColumns.join(", ")}, ` + `authored by ${authors}; caps: ${describeCaps(sweep, config.budget.dailyBudgetCents)}; ` + `${sweep.maxProbesPerTick} probes/tick`;
|
|
13975
14082
|
}
|
|
13976
|
-
var
|
|
14083
|
+
var TAG36 = "sweep";
|
|
13977
14084
|
var init_sweep_guard = () => {};
|
|
13978
14085
|
|
|
13979
14086
|
// src/reconcile.ts
|
|
13980
|
-
import { detectGitProvider as detectGitProvider5, log as
|
|
14087
|
+
import { detectGitProvider as detectGitProvider5, log as log40 } from "@gethmy/harness";
|
|
13981
14088
|
function daemonProcessLabels(approvedLabel) {
|
|
13982
14089
|
const names = ["agent", "agent-recovered", NEED_REVIEW_LABEL, approvedLabel];
|
|
13983
14090
|
return new Set(names.filter(Boolean).map((n) => n.toLowerCase()));
|
|
@@ -14008,6 +14115,8 @@ class Reconciler {
|
|
|
14008
14115
|
repoDirectories = null;
|
|
14009
14116
|
lastSweepStopReported = null;
|
|
14010
14117
|
publishSweep = () => {};
|
|
14118
|
+
drainingSweepCommands = false;
|
|
14119
|
+
lastSweepNudgeAt = 0;
|
|
14011
14120
|
get lastTick() {
|
|
14012
14121
|
return this.lastTickAt;
|
|
14013
14122
|
}
|
|
@@ -14037,7 +14146,7 @@ class Reconciler {
|
|
|
14037
14146
|
clearInterval(this.timer);
|
|
14038
14147
|
this.timer = null;
|
|
14039
14148
|
}
|
|
14040
|
-
|
|
14149
|
+
log40.info(TAG37, "Heartbeat stopped");
|
|
14041
14150
|
}
|
|
14042
14151
|
async recoverStaleRuns() {
|
|
14043
14152
|
if (!this.stateStore || !this.agentConfig)
|
|
@@ -14048,7 +14157,7 @@ class Reconciler {
|
|
|
14048
14157
|
const pool = this.pool;
|
|
14049
14158
|
for (const run of active) {
|
|
14050
14159
|
if (isBudgetHeldRun(run)) {
|
|
14051
|
-
|
|
14160
|
+
log40.info(TAG37, `run ${run.runId} (#${run.cardShortId}) is held for a human budget decision — leaving it`);
|
|
14052
14161
|
continue;
|
|
14053
14162
|
}
|
|
14054
14163
|
const foreignDaemon = run.daemonPid !== process.pid;
|
|
@@ -14058,7 +14167,7 @@ class Reconciler {
|
|
|
14058
14167
|
if (!daemonDead && !(heartbeatStale && ourZombie))
|
|
14059
14168
|
continue;
|
|
14060
14169
|
const reason = daemonDead ? `foreign daemon ${run.daemonPid} is dead` : `our worker lost card ${run.cardId} with ${Math.round((now - run.lastHeartbeatAt) / 1000)}s stale heartbeat`;
|
|
14061
|
-
|
|
14170
|
+
log40.warn(TAG37, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
|
|
14062
14171
|
await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
|
|
14063
14172
|
runId: run.runId,
|
|
14064
14173
|
cardId: run.cardId,
|
|
@@ -14085,11 +14194,11 @@ class Reconciler {
|
|
|
14085
14194
|
const stalledAt = Date.parse(card.updated_at ?? "");
|
|
14086
14195
|
if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
|
|
14087
14196
|
continue;
|
|
14088
|
-
|
|
14197
|
+
log40.warn(TAG37, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
|
|
14089
14198
|
try {
|
|
14090
14199
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
14091
14200
|
} catch (err) {
|
|
14092
|
-
|
|
14201
|
+
log40.error(TAG37, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
14093
14202
|
}
|
|
14094
14203
|
}
|
|
14095
14204
|
}
|
|
@@ -14121,7 +14230,7 @@ class Reconciler {
|
|
|
14121
14230
|
return;
|
|
14122
14231
|
const cardLabels = resolveCardLabels(card, labelMap);
|
|
14123
14232
|
const subtasks = card.subtasks ?? [];
|
|
14124
|
-
|
|
14233
|
+
log40.info(TAG37, `Enqueuing claimed review card #${card.short_id} (agent-agnostic pickup)`);
|
|
14125
14234
|
await this.pool.enqueue(card, column, cardLabels, subtasks, "review");
|
|
14126
14235
|
}
|
|
14127
14236
|
});
|
|
@@ -14145,11 +14254,11 @@ class Reconciler {
|
|
|
14145
14254
|
const parkedAt = Date.parse(card.updated_at ?? "");
|
|
14146
14255
|
if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
|
|
14147
14256
|
continue;
|
|
14148
|
-
|
|
14257
|
+
log40.warn(TAG37, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
|
|
14149
14258
|
try {
|
|
14150
14259
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
14151
14260
|
} catch (err) {
|
|
14152
|
-
|
|
14261
|
+
log40.error(TAG37, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
14153
14262
|
}
|
|
14154
14263
|
}
|
|
14155
14264
|
}
|
|
@@ -14184,7 +14293,7 @@ class Reconciler {
|
|
|
14184
14293
|
}
|
|
14185
14294
|
this.lastSweepStopReported = null;
|
|
14186
14295
|
if (!this.pool.hasFreeImplementSlot()) {
|
|
14187
|
-
|
|
14296
|
+
log40.debug(TAG37, "sweep skipped — no free implement slot");
|
|
14188
14297
|
return;
|
|
14189
14298
|
}
|
|
14190
14299
|
const { claimed } = await sweepForCard({
|
|
@@ -14203,9 +14312,9 @@ class Reconciler {
|
|
|
14203
14312
|
reportSweepStopped(reason, detail) {
|
|
14204
14313
|
const line = `sweep stopped claiming — ${detail}`;
|
|
14205
14314
|
if (this.lastSweepStopReported === reason)
|
|
14206
|
-
|
|
14315
|
+
log40.debug(TAG37, line);
|
|
14207
14316
|
else
|
|
14208
|
-
|
|
14317
|
+
log40.warn(TAG37, line);
|
|
14209
14318
|
this.lastSweepStopReported = reason;
|
|
14210
14319
|
}
|
|
14211
14320
|
setSweepPresenceSink(sink) {
|
|
@@ -14214,15 +14323,47 @@ class Reconciler {
|
|
|
14214
14323
|
publishSweepState() {
|
|
14215
14324
|
this.publishSweep(toSweepPresence(this.sweepGuard?.snapshot()));
|
|
14216
14325
|
}
|
|
14326
|
+
async drainSweepCommands() {
|
|
14327
|
+
const guard = this.sweepGuard;
|
|
14328
|
+
if (!guard || !this.agentConfig?.sweep.enabled)
|
|
14329
|
+
return;
|
|
14330
|
+
if (this.drainingSweepCommands)
|
|
14331
|
+
return;
|
|
14332
|
+
this.drainingSweepCommands = true;
|
|
14333
|
+
try {
|
|
14334
|
+
const commands = await claimSweepCommands(this.client, this.agentId, this.projectId, this.stateStore?.getDaemon().daemonId ?? "");
|
|
14335
|
+
if (commands.length === 0)
|
|
14336
|
+
return;
|
|
14337
|
+
const applied = await applySweepCommands(commands, {
|
|
14338
|
+
halt: () => guard.halt("operator"),
|
|
14339
|
+
resume: () => guard.resume()
|
|
14340
|
+
});
|
|
14341
|
+
if (!applied)
|
|
14342
|
+
return;
|
|
14343
|
+
this.lastSweepStopReported = null;
|
|
14344
|
+
this.publishSweepState();
|
|
14345
|
+
} finally {
|
|
14346
|
+
this.drainingSweepCommands = false;
|
|
14347
|
+
}
|
|
14348
|
+
}
|
|
14349
|
+
onSweepCommandNudge() {
|
|
14350
|
+
const now = Date.now();
|
|
14351
|
+
if (now - this.lastSweepNudgeAt < SWEEP_NUDGE_COOLDOWN_MS)
|
|
14352
|
+
return;
|
|
14353
|
+
this.lastSweepNudgeAt = now;
|
|
14354
|
+
this.drainSweepCommands().catch((err) => {
|
|
14355
|
+
log40.error(TAG37, `sweep command nudge failed: ${err instanceof Error ? err.message : err}`);
|
|
14356
|
+
});
|
|
14357
|
+
}
|
|
14217
14358
|
resolveRepoDirectories() {
|
|
14218
14359
|
if (this.repoDirectories)
|
|
14219
14360
|
return this.repoDirectories;
|
|
14220
14361
|
const dirs = readRepoDirectories(process.cwd());
|
|
14221
14362
|
this.repoDirectories = dirs;
|
|
14222
14363
|
if (dirs.size === 0) {
|
|
14223
|
-
|
|
14364
|
+
log40.warn(TAG37, `Could not read top-level directories of ${process.cwd()} — sweep cohesion runs without its path signal`);
|
|
14224
14365
|
} else {
|
|
14225
|
-
|
|
14366
|
+
log40.debug(TAG37, `Path signal allows ${dirs.size} top-level directories`);
|
|
14226
14367
|
}
|
|
14227
14368
|
return dirs;
|
|
14228
14369
|
}
|
|
@@ -14266,21 +14407,21 @@ class Reconciler {
|
|
|
14266
14407
|
const subtasks = card.subtasks ?? [];
|
|
14267
14408
|
const mode = route.mode;
|
|
14268
14409
|
if (route.stage) {
|
|
14269
|
-
|
|
14410
|
+
log40.info(TAG37, `Stage card #${card.short_id} (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement) regardless of column`);
|
|
14270
14411
|
}
|
|
14271
14412
|
if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
|
|
14272
|
-
|
|
14413
|
+
log40.debug(TAG37, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
|
|
14273
14414
|
continue;
|
|
14274
14415
|
}
|
|
14275
14416
|
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
14276
|
-
|
|
14417
|
+
log40.debug(TAG37, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
|
|
14277
14418
|
continue;
|
|
14278
14419
|
}
|
|
14279
14420
|
if (mode === "review" && !qualifiesForAutoReview(card.description)) {
|
|
14280
|
-
|
|
14421
|
+
log40.debug(TAG37, `Skipping #${card.short_id} — no branch or PR reference (not qualified for auto-review)`);
|
|
14281
14422
|
continue;
|
|
14282
14423
|
}
|
|
14283
|
-
|
|
14424
|
+
log40.info(TAG37, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
|
|
14284
14425
|
await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
14285
14426
|
}
|
|
14286
14427
|
}
|
|
@@ -14290,29 +14431,34 @@ class Reconciler {
|
|
|
14290
14431
|
try {
|
|
14291
14432
|
await this.pool.drainBudgetDecisions();
|
|
14292
14433
|
} catch (err) {
|
|
14293
|
-
|
|
14434
|
+
log40.error(TAG37, `budget decisions were not drained this tick: ${err instanceof Error ? err.message : err}`);
|
|
14294
14435
|
}
|
|
14295
14436
|
await this.recoverStrandedInProgress(cards, columns, knownCardIds);
|
|
14296
14437
|
await this.recoverStrandedReview(cards, columns, labelMap, knownCardIds);
|
|
14297
14438
|
for (const knownId of knownCardIds) {
|
|
14298
14439
|
if (!allAgentCardIds.has(knownId)) {
|
|
14299
|
-
|
|
14440
|
+
log40.info(TAG37, `Missed unassign: ${knownId} — removing`);
|
|
14300
14441
|
await this.pool.removeCard(knownId);
|
|
14301
14442
|
}
|
|
14302
14443
|
}
|
|
14303
14444
|
await this.releaseStalledApprovals(cards, columns, knownCardIds);
|
|
14445
|
+
try {
|
|
14446
|
+
await this.drainSweepCommands();
|
|
14447
|
+
} catch (err) {
|
|
14448
|
+
log40.error(TAG37, `sweep commands were not drained this tick: ${err instanceof Error ? err.message : err}`);
|
|
14449
|
+
}
|
|
14304
14450
|
try {
|
|
14305
14451
|
await this.sweepForWork(cards, columns, labelMap, pickupConfig);
|
|
14306
14452
|
} catch (err) {
|
|
14307
|
-
|
|
14453
|
+
log40.error(TAG37, `sweep failed this tick: ${err instanceof Error ? err.message : err}`);
|
|
14308
14454
|
}
|
|
14309
|
-
|
|
14455
|
+
log40.debug(TAG37, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
|
|
14310
14456
|
} catch (err) {
|
|
14311
|
-
|
|
14457
|
+
log40.error(TAG37, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
|
|
14312
14458
|
}
|
|
14313
14459
|
}
|
|
14314
14460
|
}
|
|
14315
|
-
var
|
|
14461
|
+
var TAG37 = "reconcile", SWEEP_NUDGE_COOLDOWN_MS = 15000;
|
|
14316
14462
|
var init_reconcile = __esm(() => {
|
|
14317
14463
|
init_board_helpers();
|
|
14318
14464
|
init_recovery();
|
|
@@ -14321,6 +14467,7 @@ var init_reconcile = __esm(() => {
|
|
|
14321
14467
|
init_state_store();
|
|
14322
14468
|
init_strand_recovery();
|
|
14323
14469
|
init_sweep();
|
|
14470
|
+
init_sweep_commands();
|
|
14324
14471
|
init_sweep_guard();
|
|
14325
14472
|
init_types2();
|
|
14326
14473
|
});
|
|
@@ -14331,7 +14478,7 @@ __export(exports_startup_banner, {
|
|
|
14331
14478
|
createStartupBanner: () => createStartupBanner,
|
|
14332
14479
|
contextSourceLine: () => contextSourceLine
|
|
14333
14480
|
});
|
|
14334
|
-
import { isPretty, log as
|
|
14481
|
+
import { isPretty, log as log41 } from "@gethmy/harness";
|
|
14335
14482
|
function createStartupBanner(config, version) {
|
|
14336
14483
|
return isPretty() ? prettyBanner(config, version) : jsonBanner(config, version);
|
|
14337
14484
|
}
|
|
@@ -14356,7 +14503,7 @@ function prettyBanner(config, version) {
|
|
|
14356
14503
|
checks.push({ kind: "ok", message });
|
|
14357
14504
|
},
|
|
14358
14505
|
warn(message) {
|
|
14359
|
-
|
|
14506
|
+
log41.warn(TAG38, message);
|
|
14360
14507
|
checks.push({ kind: "warn", message: message.split(`
|
|
14361
14508
|
`, 1)[0] });
|
|
14362
14509
|
},
|
|
@@ -14381,26 +14528,26 @@ function prettyBanner(config, version) {
|
|
|
14381
14528
|
};
|
|
14382
14529
|
}
|
|
14383
14530
|
function jsonBanner(config, version) {
|
|
14384
|
-
|
|
14385
|
-
|
|
14386
|
-
|
|
14531
|
+
log41.info(TAG38, `Harmony Agent Daemon v${version} starting...`);
|
|
14532
|
+
log41.info(TAG38, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
|
|
14533
|
+
log41.info(TAG38, `Config: ${contextSourceLine(config)}`);
|
|
14387
14534
|
if (config.agent.review.enabled) {
|
|
14388
|
-
|
|
14535
|
+
log41.info(TAG38, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
|
|
14389
14536
|
}
|
|
14390
14537
|
let failed = false;
|
|
14391
14538
|
return {
|
|
14392
14539
|
setProjectName(_name) {},
|
|
14393
14540
|
setGitProvider(provider) {
|
|
14394
|
-
|
|
14541
|
+
log41.info(TAG38, `Git provider: ${provider}`);
|
|
14395
14542
|
},
|
|
14396
14543
|
setHttpPort(port) {
|
|
14397
|
-
|
|
14544
|
+
log41.info(TAG38, `HTTP server on port ${port}`);
|
|
14398
14545
|
},
|
|
14399
14546
|
check(message) {
|
|
14400
|
-
|
|
14547
|
+
log41.info(TAG38, message);
|
|
14401
14548
|
},
|
|
14402
14549
|
warn(message) {
|
|
14403
|
-
|
|
14550
|
+
log41.warn(TAG38, message);
|
|
14404
14551
|
},
|
|
14405
14552
|
fail() {
|
|
14406
14553
|
failed = true;
|
|
@@ -14408,7 +14555,7 @@ function jsonBanner(config, version) {
|
|
|
14408
14555
|
async ready(message) {
|
|
14409
14556
|
if (failed)
|
|
14410
14557
|
return;
|
|
14411
|
-
|
|
14558
|
+
log41.info(TAG38, message);
|
|
14412
14559
|
}
|
|
14413
14560
|
};
|
|
14414
14561
|
}
|
|
@@ -14460,7 +14607,7 @@ function configRows(config, projectName, gitProvider, httpPort) {
|
|
|
14460
14607
|
if (gitProvider)
|
|
14461
14608
|
tail.push(gitProvider);
|
|
14462
14609
|
if (config.agent.http.enabled) {
|
|
14463
|
-
tail.push(`HTTP
|
|
14610
|
+
tail.push(`HTTP ${daemonBaseUrl(config.agent.http.bindAddr, httpPort ?? config.agent.http.port)}`);
|
|
14464
14611
|
}
|
|
14465
14612
|
if (tail.length > 0) {
|
|
14466
14613
|
rows.push({ label: "Git", value: tail.join(" · ") });
|
|
@@ -14497,8 +14644,9 @@ function cyan(s) {
|
|
|
14497
14644
|
function yellow(s) {
|
|
14498
14645
|
return `${ANSI.yellow}${s}${ANSI.reset}`;
|
|
14499
14646
|
}
|
|
14500
|
-
var
|
|
14647
|
+
var TAG38 = "daemon", RULE_WIDTH = 70, ANSI;
|
|
14501
14648
|
var init_startup_banner = __esm(() => {
|
|
14649
|
+
init_port_registry();
|
|
14502
14650
|
ANSI = {
|
|
14503
14651
|
reset: "\x1B[0m",
|
|
14504
14652
|
dim: "\x1B[2m",
|
|
@@ -14517,7 +14665,7 @@ import {
|
|
|
14517
14665
|
writeFileSync as writeFileSync5
|
|
14518
14666
|
} from "node:fs";
|
|
14519
14667
|
import { dirname as dirname6, resolve as resolve2, sep as sep2 } from "node:path";
|
|
14520
|
-
import { log as
|
|
14668
|
+
import { log as log42 } from "@gethmy/harness";
|
|
14521
14669
|
function migrationMarkerPath(targetPath) {
|
|
14522
14670
|
return `${targetPath}.migrated`;
|
|
14523
14671
|
}
|
|
@@ -14592,7 +14740,7 @@ function migrateLegacyStateFile(opts) {
|
|
|
14592
14740
|
throw new Error("not an object");
|
|
14593
14741
|
legacy = parsed;
|
|
14594
14742
|
} catch (err) {
|
|
14595
|
-
|
|
14743
|
+
log42.warn(TAG39, `legacy state file unreadable — skipping the split this start; it retries on the next (${err instanceof Error ? err.message : err})`);
|
|
14596
14744
|
return NO_MIGRATION;
|
|
14597
14745
|
}
|
|
14598
14746
|
const runs = Array.isArray(legacy.runs) ? legacy.runs : [];
|
|
@@ -14614,13 +14762,13 @@ function migrateLegacyStateFile(opts) {
|
|
|
14614
14762
|
const legacyVersion = typeof legacy.version === "number" ? legacy.version : SCHEMA_VERSION;
|
|
14615
14763
|
const existing = existsSync4(targetPath) ? readTargetFile(targetPath) : null;
|
|
14616
14764
|
if (existsSync4(targetPath) && !existing) {
|
|
14617
|
-
|
|
14765
|
+
log42.warn(TAG39, "existing project state file unreadable — the split overwrites it");
|
|
14618
14766
|
}
|
|
14619
14767
|
let target;
|
|
14620
14768
|
if (existing) {
|
|
14621
14769
|
const existingVersion = typeof existing.version === "number" ? existing.version : SCHEMA_VERSION;
|
|
14622
14770
|
if (existingVersion !== legacyVersion) {
|
|
14623
|
-
|
|
14771
|
+
log42.warn(TAG39, `legacy state is schema v${legacyVersion} but the project store is v${existingVersion} — skipping the split`);
|
|
14624
14772
|
return NO_MIGRATION;
|
|
14625
14773
|
}
|
|
14626
14774
|
const existingRuns = Array.isArray(existing.runs) ? existing.runs : [];
|
|
@@ -14666,7 +14814,7 @@ function migrateLegacyStateFile(opts) {
|
|
|
14666
14814
|
if (!existsSync4(backupPath))
|
|
14667
14815
|
copyFileSync(legacyPath, backupPath);
|
|
14668
14816
|
} catch (err) {
|
|
14669
|
-
|
|
14817
|
+
log42.warn(TAG39, `could not back up the legacy state file (continuing): ${err instanceof Error ? err.message : err}`);
|
|
14670
14818
|
}
|
|
14671
14819
|
const remainingRuns = runs.filter((r) => !ownRunIds.has(r.runId));
|
|
14672
14820
|
const remainingCards = cards.filter((c) => !ownCardIds.has(c.cardId));
|
|
@@ -14690,10 +14838,10 @@ function migrateLegacyStateFile(opts) {
|
|
|
14690
14838
|
copiedIndeterminateCards: indeterminateCards.length,
|
|
14691
14839
|
foreignUnendedRuns: remainingRuns.filter((r) => r.endedAt === null).length
|
|
14692
14840
|
};
|
|
14693
|
-
|
|
14841
|
+
log42.info(TAG39, `split legacy state into ${targetPath}: moved ${result.movedRuns} run(s) ` + `(${result.movedUnendedRuns} unended) and ${result.movedCards} card record(s), ` + `copied ${result.copiedIndeterminateCards} unattributable card record(s); ` + `left ${remainingRuns.length} run(s) (${result.foreignUnendedRuns} unended) for other projects`);
|
|
14694
14842
|
return result;
|
|
14695
14843
|
}
|
|
14696
|
-
var
|
|
14844
|
+
var TAG39 = "state-migration", NO_MIGRATION;
|
|
14697
14845
|
var init_state_migration = __esm(() => {
|
|
14698
14846
|
init_state_store();
|
|
14699
14847
|
NO_MIGRATION = {
|
|
@@ -14799,7 +14947,7 @@ var init_stream_parser_selftest = __esm(() => {
|
|
|
14799
14947
|
|
|
14800
14948
|
// src/watcher.ts
|
|
14801
14949
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
14802
|
-
import { isPretty as isPretty2, log as
|
|
14950
|
+
import { isPretty as isPretty2, log as log43 } from "@gethmy/harness";
|
|
14803
14951
|
import { createClient } from "@supabase/supabase-js";
|
|
14804
14952
|
function toPresenceColumns(names) {
|
|
14805
14953
|
return names.map((n) => n.trim()).filter((n) => n.length > 0).slice(0, MAX_PRESENCE_COLUMNS).map((n) => n.slice(0, MAX_PRESENCE_COLUMN_LENGTH));
|
|
@@ -14812,6 +14960,7 @@ class Watcher {
|
|
|
14812
14960
|
routing;
|
|
14813
14961
|
onCardBroadcast;
|
|
14814
14962
|
onAgentCommand;
|
|
14963
|
+
onSweepCommandNudge;
|
|
14815
14964
|
channel = null;
|
|
14816
14965
|
presenceChannel = null;
|
|
14817
14966
|
supabase = null;
|
|
@@ -14859,20 +15008,21 @@ class Watcher {
|
|
|
14859
15008
|
const stillHeld = this.supabase?.getChannels().some((c) => c.topic === old.topic) ?? false;
|
|
14860
15009
|
if (!stillHeld)
|
|
14861
15010
|
return true;
|
|
14862
|
-
|
|
15011
|
+
log43.warn(TAG40, `removeChannel(${old.topic}) returned "${status}" and the client still holds that topic — retrying the removal`);
|
|
14863
15012
|
return false;
|
|
14864
15013
|
}
|
|
14865
|
-
constructor(credentials, projectId, identity, routing, onCardBroadcast, onAgentCommand) {
|
|
15014
|
+
constructor(credentials, projectId, identity, routing, onCardBroadcast, onAgentCommand, onSweepCommandNudge) {
|
|
14866
15015
|
this.credentials = credentials;
|
|
14867
15016
|
this.projectId = projectId;
|
|
14868
15017
|
this.identity = identity;
|
|
14869
15018
|
this.routing = routing;
|
|
14870
15019
|
this.onCardBroadcast = onCardBroadcast;
|
|
14871
15020
|
this.onAgentCommand = onAgentCommand;
|
|
15021
|
+
this.onSweepCommandNudge = onSweepCommandNudge;
|
|
14872
15022
|
}
|
|
14873
15023
|
async start() {
|
|
14874
15024
|
if (!isPretty2()) {
|
|
14875
|
-
|
|
15025
|
+
log43.info(TAG40, "Connecting to Supabase realtime (broadcast)...");
|
|
14876
15026
|
}
|
|
14877
15027
|
this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
|
|
14878
15028
|
this.subscribeBroadcast();
|
|
@@ -14910,7 +15060,7 @@ class Watcher {
|
|
|
14910
15060
|
status = `threw (${String(err)})`;
|
|
14911
15061
|
}
|
|
14912
15062
|
if (status !== "ok") {
|
|
14913
|
-
|
|
15063
|
+
log43.debug(TAG40, `Presence sweep update returned "${status}"`);
|
|
14914
15064
|
}
|
|
14915
15065
|
})();
|
|
14916
15066
|
}
|
|
@@ -14921,7 +15071,7 @@ class Watcher {
|
|
|
14921
15071
|
const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
|
|
14922
15072
|
this.presenceChannel = presenceChannel;
|
|
14923
15073
|
presenceChannel.on("presence", { event: "sync" }, () => {
|
|
14924
|
-
|
|
15074
|
+
log43.debug(TAG40, "Presence sync");
|
|
14925
15075
|
}).subscribe(async (status) => {
|
|
14926
15076
|
if (gen !== this.presenceGen)
|
|
14927
15077
|
return;
|
|
@@ -14937,13 +15087,13 @@ class Watcher {
|
|
|
14937
15087
|
if (trackStatus !== "ok") {
|
|
14938
15088
|
this.presenceTracked = false;
|
|
14939
15089
|
if (!this.stopping) {
|
|
14940
|
-
|
|
15090
|
+
log43.warn(TAG40, `Presence track returned "${trackStatus}" — scheduling reconnect`);
|
|
14941
15091
|
this.schedulePresenceReconnect();
|
|
14942
15092
|
}
|
|
14943
15093
|
return;
|
|
14944
15094
|
}
|
|
14945
15095
|
if (!isPretty2() || !this.suppressStartupLogs) {
|
|
14946
|
-
|
|
15096
|
+
log43.info(TAG40, "Presence tracked on board-presence channel");
|
|
14947
15097
|
}
|
|
14948
15098
|
this.presenceTracked = true;
|
|
14949
15099
|
this.presenceReconnectAttempts = 0;
|
|
@@ -14951,7 +15101,7 @@ class Watcher {
|
|
|
14951
15101
|
} else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
|
|
14952
15102
|
this.presenceTracked = false;
|
|
14953
15103
|
if (!this.stopping) {
|
|
14954
|
-
|
|
15104
|
+
log43.warn(TAG40, `Presence subscription ${status} — scheduling reconnect`);
|
|
14955
15105
|
this.schedulePresenceReconnect();
|
|
14956
15106
|
}
|
|
14957
15107
|
}
|
|
@@ -14970,7 +15120,7 @@ class Watcher {
|
|
|
14970
15120
|
async reconnectPresence() {
|
|
14971
15121
|
if (this.stopping || !this.supabase)
|
|
14972
15122
|
return;
|
|
14973
|
-
|
|
15123
|
+
log43.warn(TAG40, `Reconnecting presence subscription (attempt ${this.presenceReconnectAttempts})`);
|
|
14974
15124
|
if (this.presenceChannel) {
|
|
14975
15125
|
const old = this.presenceChannel;
|
|
14976
15126
|
this.presenceChannel = null;
|
|
@@ -14993,13 +15143,13 @@ class Watcher {
|
|
|
14993
15143
|
return;
|
|
14994
15144
|
const gen = ++this.broadcastGen;
|
|
14995
15145
|
this.channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
|
|
14996
|
-
|
|
15146
|
+
log43.debug(TAG40, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
|
|
14997
15147
|
this.onCardBroadcast({
|
|
14998
15148
|
event: "card_update",
|
|
14999
15149
|
payload: msg.payload ?? {}
|
|
15000
15150
|
});
|
|
15001
15151
|
}).on("broadcast", { event: "card_created" }, (msg) => {
|
|
15002
|
-
|
|
15152
|
+
log43.debug(TAG40, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
|
|
15003
15153
|
this.onCardBroadcast({
|
|
15004
15154
|
event: "card_created",
|
|
15005
15155
|
payload: msg.payload ?? {}
|
|
@@ -15009,9 +15159,12 @@ class Watcher {
|
|
|
15009
15159
|
const cardId = payload.card_id;
|
|
15010
15160
|
const command = payload.command;
|
|
15011
15161
|
if (cardId && command) {
|
|
15012
|
-
|
|
15162
|
+
log43.info(TAG40, `Broadcast: agent_command ${command} for ${cardId}`);
|
|
15013
15163
|
this.onAgentCommand?.({ cardId, command });
|
|
15014
15164
|
}
|
|
15165
|
+
}).on("broadcast", { event: "sweep_command" }, () => {
|
|
15166
|
+
log43.debug(TAG40, "Broadcast: sweep_command — draining");
|
|
15167
|
+
this.onSweepCommandNudge?.();
|
|
15015
15168
|
}).subscribe((status) => {
|
|
15016
15169
|
if (gen !== this.broadcastGen)
|
|
15017
15170
|
return;
|
|
@@ -15019,13 +15172,13 @@ class Watcher {
|
|
|
15019
15172
|
this.connected = true;
|
|
15020
15173
|
this.reconnectAttempts = 0;
|
|
15021
15174
|
if (!isPretty2() || !this.suppressStartupLogs) {
|
|
15022
|
-
|
|
15175
|
+
log43.info(TAG40, "Broadcast subscription active");
|
|
15023
15176
|
}
|
|
15024
15177
|
this.maybeResolveReady();
|
|
15025
15178
|
} else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
|
|
15026
15179
|
this.connected = false;
|
|
15027
15180
|
if (!this.stopping) {
|
|
15028
|
-
|
|
15181
|
+
log43.warn(TAG40, `Broadcast subscription ${status} — scheduling reconnect`);
|
|
15029
15182
|
this.scheduleReconnect();
|
|
15030
15183
|
}
|
|
15031
15184
|
}
|
|
@@ -15044,7 +15197,7 @@ class Watcher {
|
|
|
15044
15197
|
async reconnectBroadcast() {
|
|
15045
15198
|
if (this.stopping || !this.supabase)
|
|
15046
15199
|
return;
|
|
15047
|
-
|
|
15200
|
+
log43.warn(TAG40, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
|
|
15048
15201
|
if (this.channel) {
|
|
15049
15202
|
const old = this.channel;
|
|
15050
15203
|
this.channel = null;
|
|
@@ -15086,10 +15239,10 @@ class Watcher {
|
|
|
15086
15239
|
}
|
|
15087
15240
|
this.connected = false;
|
|
15088
15241
|
this.presenceTracked = false;
|
|
15089
|
-
|
|
15242
|
+
log43.info(TAG40, "Broadcast subscription stopped");
|
|
15090
15243
|
}
|
|
15091
15244
|
}
|
|
15092
|
-
var
|
|
15245
|
+
var TAG40 = "watcher", MAX_PRESENCE_COLUMNS = 24, MAX_PRESENCE_COLUMN_LENGTH = 120;
|
|
15093
15246
|
var init_watcher = __esm(() => {
|
|
15094
15247
|
init_sweep_guard();
|
|
15095
15248
|
});
|
|
@@ -15105,7 +15258,7 @@ __export(exports_worktree_gc, {
|
|
|
15105
15258
|
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
15106
15259
|
import { existsSync as existsSync5, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
|
|
15107
15260
|
import { resolve as resolve3 } from "node:path";
|
|
15108
|
-
import { cleanupWorktree as cleanupWorktree5, GIT_NO_HOOKS as GIT_NO_HOOKS9, log as
|
|
15261
|
+
import { cleanupWorktree as cleanupWorktree5, GIT_NO_HOOKS as GIT_NO_HOOKS9, log as log44 } from "@gethmy/harness";
|
|
15109
15262
|
function isTransientGitNetworkError(message) {
|
|
15110
15263
|
return TRANSIENT_GIT_NETWORK_ERROR.test(message);
|
|
15111
15264
|
}
|
|
@@ -15218,10 +15371,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
|
|
|
15218
15371
|
});
|
|
15219
15372
|
} catch {}
|
|
15220
15373
|
if (result.removed.length > 0) {
|
|
15221
|
-
|
|
15374
|
+
log44.info(TAG41, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
|
|
15222
15375
|
}
|
|
15223
15376
|
if (result.errors.length > 0) {
|
|
15224
|
-
|
|
15377
|
+
log44.warn(TAG41, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
|
|
15225
15378
|
}
|
|
15226
15379
|
return result;
|
|
15227
15380
|
}
|
|
@@ -15251,7 +15404,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
15251
15404
|
} catch (err) {
|
|
15252
15405
|
const detail = gitErrorDetail2(err);
|
|
15253
15406
|
if (isTransientGitNetworkError(detail)) {
|
|
15254
|
-
|
|
15407
|
+
log44.debug(TAG41, `Remote branch GC skipped — remote unreachable: ${detail}`);
|
|
15255
15408
|
return result;
|
|
15256
15409
|
}
|
|
15257
15410
|
result.errors.push({ ref: "fetch", error: detail });
|
|
@@ -15291,7 +15444,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
15291
15444
|
continue;
|
|
15292
15445
|
}
|
|
15293
15446
|
if (clock() > sweepDeadline) {
|
|
15294
|
-
|
|
15447
|
+
log44.debug(TAG41, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
|
|
15295
15448
|
break;
|
|
15296
15449
|
}
|
|
15297
15450
|
try {
|
|
@@ -15304,17 +15457,17 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
15304
15457
|
} catch (err) {
|
|
15305
15458
|
const detail = gitErrorDetail2(err);
|
|
15306
15459
|
if (isTransientGitNetworkError(detail)) {
|
|
15307
|
-
|
|
15460
|
+
log44.debug(TAG41, `Remote branch GC interrupted — remote unreachable: ${detail}`);
|
|
15308
15461
|
break;
|
|
15309
15462
|
}
|
|
15310
15463
|
result.errors.push({ ref, error: detail });
|
|
15311
15464
|
}
|
|
15312
15465
|
}
|
|
15313
15466
|
if (result.removed.length > 0) {
|
|
15314
|
-
|
|
15467
|
+
log44.info(TAG41, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
|
|
15315
15468
|
}
|
|
15316
15469
|
if (result.errors.length > 0) {
|
|
15317
|
-
|
|
15470
|
+
log44.warn(TAG41, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
|
|
15318
15471
|
}
|
|
15319
15472
|
return result;
|
|
15320
15473
|
}
|
|
@@ -15345,13 +15498,13 @@ class WorktreeGc {
|
|
|
15345
15498
|
try {
|
|
15346
15499
|
runWorktreeGc(this.basePath, this.store);
|
|
15347
15500
|
} catch (err) {
|
|
15348
|
-
|
|
15501
|
+
log44.warn(TAG41, `GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
15349
15502
|
}
|
|
15350
15503
|
if (this.remoteOpts) {
|
|
15351
15504
|
try {
|
|
15352
15505
|
pruneFailedRemoteBranches(this.remoteOpts);
|
|
15353
15506
|
} catch (err) {
|
|
15354
|
-
|
|
15507
|
+
log44.warn(TAG41, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
15355
15508
|
}
|
|
15356
15509
|
}
|
|
15357
15510
|
}
|
|
@@ -15365,7 +15518,7 @@ function getRepoRoot2() {
|
|
|
15365
15518
|
return null;
|
|
15366
15519
|
}
|
|
15367
15520
|
}
|
|
15368
|
-
var
|
|
15521
|
+
var TAG41 = "worktree-gc", GIT_NETWORK_TIMEOUT_MS = 30000, GIT_SSH_CONNECT_TIMEOUT_SECS = 10, GIT_PRUNE_SWEEP_BUDGET_MS = 60000, GIT_NETWORK_EXEC, TRANSIENT_GIT_NETWORK_ERROR;
|
|
15369
15522
|
var init_worktree_gc = __esm(() => {
|
|
15370
15523
|
GIT_NETWORK_EXEC = {
|
|
15371
15524
|
timeout: GIT_NETWORK_TIMEOUT_MS,
|
|
@@ -15404,7 +15557,7 @@ import { resolve as resolve4 } from "node:path";
|
|
|
15404
15557
|
import {
|
|
15405
15558
|
detectGitProvider as detectGitProvider6,
|
|
15406
15559
|
GIT_NO_HOOKS as GIT_NO_HOOKS10,
|
|
15407
|
-
log as
|
|
15560
|
+
log as log45,
|
|
15408
15561
|
validateGitProviderCli
|
|
15409
15562
|
} from "@gethmy/harness";
|
|
15410
15563
|
async function validatePrerequisites(config, banner) {
|
|
@@ -15481,7 +15634,7 @@ async function main() {
|
|
|
15481
15634
|
} catch (err) {
|
|
15482
15635
|
if (err instanceof ConfigValidationError) {
|
|
15483
15636
|
banner.fail();
|
|
15484
|
-
|
|
15637
|
+
log45.error(TAG42, err.message);
|
|
15485
15638
|
process.exit(1);
|
|
15486
15639
|
}
|
|
15487
15640
|
throw err;
|
|
@@ -15492,10 +15645,11 @@ async function main() {
|
|
|
15492
15645
|
validateSweepConfig(config.agent);
|
|
15493
15646
|
validateRankingConfig(config.agent);
|
|
15494
15647
|
validateVerificationConfig(config.agent);
|
|
15648
|
+
validateHttpConfig(config.agent);
|
|
15495
15649
|
} catch (err) {
|
|
15496
15650
|
if (err instanceof ConfigValidationError) {
|
|
15497
15651
|
banner.fail();
|
|
15498
|
-
|
|
15652
|
+
log45.error(TAG42, err.message);
|
|
15499
15653
|
process.exit(1);
|
|
15500
15654
|
}
|
|
15501
15655
|
throw err;
|
|
@@ -15523,7 +15677,7 @@ async function main() {
|
|
|
15523
15677
|
worktreePrefix: resolve4(config.repoRoot, config.agent.worktree.basePath)
|
|
15524
15678
|
});
|
|
15525
15679
|
} catch (err) {
|
|
15526
|
-
|
|
15680
|
+
log45.error(TAG42, `legacy state split failed — starting with the project store as-is: ${err instanceof Error ? err.message : err}`);
|
|
15527
15681
|
}
|
|
15528
15682
|
const stateStore = StateStore.open({ projectId: config.projectId });
|
|
15529
15683
|
const daemonId = randomUUID4();
|
|
@@ -15667,14 +15821,14 @@ async function main() {
|
|
|
15667
15821
|
await handleBroadcast(event, client, pool, config, agentId);
|
|
15668
15822
|
}, async (command) => {
|
|
15669
15823
|
await pool.handleAgentCommand(command.cardId, command.command);
|
|
15670
|
-
});
|
|
15824
|
+
}, () => reconciler.onSweepCommandNudge());
|
|
15671
15825
|
let shuttingDown = false;
|
|
15672
15826
|
let exitCode = 0;
|
|
15673
15827
|
const shutdown = async (signal) => {
|
|
15674
15828
|
if (shuttingDown)
|
|
15675
15829
|
return;
|
|
15676
15830
|
shuttingDown = true;
|
|
15677
|
-
|
|
15831
|
+
log45.info(TAG42, `Received ${signal}, shutting down gracefully...`);
|
|
15678
15832
|
reconciler.stop();
|
|
15679
15833
|
mergeMonitor?.stop();
|
|
15680
15834
|
worktreeGc.stop();
|
|
@@ -15685,19 +15839,19 @@ async function main() {
|
|
|
15685
15839
|
}
|
|
15686
15840
|
await watcher.stop();
|
|
15687
15841
|
await pool.shutdown();
|
|
15688
|
-
|
|
15842
|
+
log45.info(TAG42, "Daemon stopped.");
|
|
15689
15843
|
process.exit(exitCode);
|
|
15690
15844
|
};
|
|
15691
15845
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
15692
15846
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
15693
15847
|
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
15694
15848
|
process.on("uncaughtException", (err) => {
|
|
15695
|
-
|
|
15849
|
+
log45.error(TAG42, `Uncaught exception: ${err.message}`);
|
|
15696
15850
|
exitCode = 1;
|
|
15697
15851
|
shutdown("uncaughtException");
|
|
15698
15852
|
});
|
|
15699
15853
|
process.on("unhandledRejection", (reason) => {
|
|
15700
|
-
|
|
15854
|
+
log45.error(TAG42, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
15701
15855
|
exitCode = 1;
|
|
15702
15856
|
shutdown("unhandledRejection");
|
|
15703
15857
|
});
|
|
@@ -15757,29 +15911,29 @@ async function handleBroadcast(event, client, pool, config, agentId) {
|
|
|
15757
15911
|
if (assignedAgentId === undefined)
|
|
15758
15912
|
return;
|
|
15759
15913
|
if (assignedAgentId === agentId) {
|
|
15760
|
-
|
|
15914
|
+
log45.info(TAG42, `Broadcast: card ${cardId} assigned to agent`);
|
|
15761
15915
|
try {
|
|
15762
15916
|
await pool.resetAttemptsForReassign(cardId);
|
|
15763
15917
|
await tryEnqueueCard(cardId, client, pool, config, agentId);
|
|
15764
15918
|
} catch (err) {
|
|
15765
|
-
|
|
15919
|
+
log45.error(TAG42, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
|
|
15766
15920
|
}
|
|
15767
15921
|
} else if (pool.isCardKnown(cardId)) {
|
|
15768
|
-
|
|
15922
|
+
log45.info(TAG42, `Broadcast: card ${cardId} unassigned from agent`);
|
|
15769
15923
|
await pool.removeCard(cardId);
|
|
15770
15924
|
}
|
|
15771
15925
|
}
|
|
15772
15926
|
async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
15773
15927
|
const { card } = await client.getCard(cardId);
|
|
15774
15928
|
if (card.assigned_agent_id !== agentId) {
|
|
15775
|
-
|
|
15929
|
+
log45.debug(TAG42, `Card ${cardId} no longer assigned to agent — skipping`);
|
|
15776
15930
|
return;
|
|
15777
15931
|
}
|
|
15778
15932
|
const board = await client.getBoard(config.projectId, { summary: true });
|
|
15779
15933
|
const columns = board.columns;
|
|
15780
15934
|
const column = columns.find((c) => c.id === card.column_id);
|
|
15781
15935
|
if (!column) {
|
|
15782
|
-
|
|
15936
|
+
log45.warn(TAG42, `Column not found for card ${cardId}`);
|
|
15783
15937
|
return;
|
|
15784
15938
|
}
|
|
15785
15939
|
const route = classifyPickup(card, column.name, {
|
|
@@ -15788,31 +15942,31 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
|
15788
15942
|
playbooks: config.agent.playbooks
|
|
15789
15943
|
});
|
|
15790
15944
|
if (!route) {
|
|
15791
|
-
|
|
15945
|
+
log45.info(TAG42, `Card #${card.short_id} is in "${column.name}", not a pickup/review/stage column — skipping`);
|
|
15792
15946
|
return;
|
|
15793
15947
|
}
|
|
15794
15948
|
if (route.stage) {
|
|
15795
|
-
|
|
15949
|
+
log45.info(TAG42, `Card #${card.short_id} is a playbook stage card (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement pool) regardless of column`);
|
|
15796
15950
|
}
|
|
15797
15951
|
const mode = route.mode;
|
|
15798
15952
|
const labelMap = buildLabelMap(board.labels ?? []);
|
|
15799
15953
|
const cardLabels = resolveCardLabels(card, labelMap);
|
|
15800
15954
|
const subtasks = card.subtasks ?? [];
|
|
15801
15955
|
if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
|
|
15802
|
-
|
|
15956
|
+
log45.debug(TAG42, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
|
|
15803
15957
|
return;
|
|
15804
15958
|
}
|
|
15805
15959
|
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
15806
|
-
|
|
15960
|
+
log45.debug(TAG42, `Card #${card.short_id} has "${NEED_REVIEW_LABEL}" label (needs human) — skipping review`);
|
|
15807
15961
|
return;
|
|
15808
15962
|
}
|
|
15809
15963
|
if (mode === "review" && !qualifiesForAutoReview(card.description)) {
|
|
15810
|
-
|
|
15964
|
+
log45.info(TAG42, `Card #${card.short_id} has no branch or PR reference — skipping auto-review`);
|
|
15811
15965
|
return;
|
|
15812
15966
|
}
|
|
15813
15967
|
await pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
15814
15968
|
}
|
|
15815
|
-
var
|
|
15969
|
+
var TAG42 = "daemon", BASE_REMOTE = "origin", PKG_VERSION;
|
|
15816
15970
|
var init_src = __esm(() => {
|
|
15817
15971
|
init_account_guard();
|
|
15818
15972
|
init_account_probe();
|
|
@@ -16106,7 +16260,7 @@ var init_run_stats = () => {};
|
|
|
16106
16260
|
// src/cli.ts
|
|
16107
16261
|
import { realpathSync } from "node:fs";
|
|
16108
16262
|
import { fileURLToPath } from "node:url";
|
|
16109
|
-
import { log as
|
|
16263
|
+
import { log as log46 } from "@gethmy/harness";
|
|
16110
16264
|
var USAGE = `
|
|
16111
16265
|
Harmony Agent — push-based daemon + ops toolkit.
|
|
16112
16266
|
|
|
@@ -16150,12 +16304,12 @@ async function runDaemon() {
|
|
|
16150
16304
|
}
|
|
16151
16305
|
async function httpCall(path, init) {
|
|
16152
16306
|
const { loadDaemonConfig: loadDaemonConfig2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
16153
|
-
const { lookupDaemonPort: lookupDaemonPort2 } = await Promise.resolve().then(() => (init_port_registry(), exports_port_registry));
|
|
16307
|
+
const { daemonBaseUrl: daemonBaseUrl2, lookupDaemonPort: lookupDaemonPort2 } = await Promise.resolve().then(() => (init_port_registry(), exports_port_registry));
|
|
16154
16308
|
const cfg = loadDaemonConfig2();
|
|
16155
16309
|
const recorded = lookupDaemonPort2(cfg.projectId);
|
|
16156
16310
|
const bindAddr = recorded?.bindAddr ?? cfg.agent.http.bindAddr;
|
|
16157
16311
|
const port = recorded?.port ?? cfg.agent.http.port;
|
|
16158
|
-
const url =
|
|
16312
|
+
const url = `${daemonBaseUrl2(bindAddr, port)}${path}`;
|
|
16159
16313
|
return fetch(url, init);
|
|
16160
16314
|
}
|
|
16161
16315
|
function formatAccount(account) {
|
|
@@ -16319,6 +16473,7 @@ async function doctorCommand() {
|
|
|
16319
16473
|
const {
|
|
16320
16474
|
validateAutoMergeConfig: validateAutoMergeConfig2,
|
|
16321
16475
|
validateBudgetConfig: validateBudgetConfig2,
|
|
16476
|
+
validateHttpConfig: validateHttpConfig2,
|
|
16322
16477
|
validateRankingConfig: validateRankingConfig2,
|
|
16323
16478
|
validateSweepConfig: validateSweepConfig2,
|
|
16324
16479
|
validateVerificationConfig: validateVerificationConfig2
|
|
@@ -16333,6 +16488,7 @@ async function doctorCommand() {
|
|
|
16333
16488
|
validateSweepConfig2(config.agent);
|
|
16334
16489
|
validateRankingConfig2(config.agent);
|
|
16335
16490
|
validateVerificationConfig2(config.agent);
|
|
16491
|
+
validateHttpConfig2(config.agent);
|
|
16336
16492
|
const banner = createStartupBanner2(config, version);
|
|
16337
16493
|
const userId = await validatePrerequisites2(config, banner);
|
|
16338
16494
|
await banner.ready(`preflight ok — agent user ${userId}`);
|
|
@@ -16755,7 +16911,7 @@ if (isMainModule()) {
|
|
|
16755
16911
|
if (code !== 0)
|
|
16756
16912
|
process.exit(code);
|
|
16757
16913
|
}).catch((err) => {
|
|
16758
|
-
|
|
16914
|
+
log46.error("cli", err instanceof Error ? err.message : String(err));
|
|
16759
16915
|
process.exit(1);
|
|
16760
16916
|
});
|
|
16761
16917
|
}
|