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