@gethmy/mcp 3.3.0 → 3.4.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 +39 -0
- package/dist/cli.js +1012 -178
- package/dist/index.js +630 -63
- package/dist/run-hook-cli.js +742 -0
- package/package.json +4 -3
- package/src/api-client.ts +26 -1
- package/src/auto-session.ts +33 -0
- package/src/cli.ts +104 -0
- package/src/hook-install.ts +388 -0
- package/src/plan-task-link.ts +3 -1
- package/src/run-event-forwarder.ts +363 -0
- package/src/run-hook-cli.ts +55 -0
- package/src/run-hook-main.ts +159 -0
- package/src/run-hook.ts +203 -0
- package/src/run-redaction.ts +461 -0
- package/src/run-state.ts +679 -0
- package/src/server.ts +85 -5
package/dist/index.js
CHANGED
|
@@ -978,6 +978,353 @@ var init_oauth_refresh = __esm(() => {
|
|
|
978
978
|
init_oauth_login();
|
|
979
979
|
});
|
|
980
980
|
|
|
981
|
+
// src/run-state.ts
|
|
982
|
+
var exports_run_state = {};
|
|
983
|
+
__export(exports_run_state, {
|
|
984
|
+
writeSpoolBatch: () => writeSpoolBatch,
|
|
985
|
+
writeRouteMemo: () => writeRouteMemo,
|
|
986
|
+
trimSpool: () => trimSpool,
|
|
987
|
+
spoolDir: () => spoolDir,
|
|
988
|
+
sessionsDir: () => sessionsDir,
|
|
989
|
+
runStateExists: () => runStateExists,
|
|
990
|
+
runStateDir: () => runStateDir,
|
|
991
|
+
removeSpoolBatches: () => removeSpoolBatches,
|
|
992
|
+
readSpoolBatches: () => readSpoolBatches,
|
|
993
|
+
readRouteMemo: () => readRouteMemo,
|
|
994
|
+
readPublishedSessions: () => readPublishedSessions,
|
|
995
|
+
publishRunSession: () => publishRunSession,
|
|
996
|
+
pidIsAlive: () => pidIsAlive,
|
|
997
|
+
clearRunSession: () => clearRunSession,
|
|
998
|
+
chooseRunSessionForHook: () => chooseRunSessionForHook,
|
|
999
|
+
ancestorPids: () => ancestorPids,
|
|
1000
|
+
RUN_STATE_DIR_ENV: () => RUN_STATE_DIR_ENV,
|
|
1001
|
+
MAX_POINTER_AGE_MS: () => MAX_POINTER_AGE_MS
|
|
1002
|
+
});
|
|
1003
|
+
import { execFileSync } from "node:child_process";
|
|
1004
|
+
import {
|
|
1005
|
+
existsSync as existsSync3,
|
|
1006
|
+
mkdirSync as mkdirSync3,
|
|
1007
|
+
readdirSync as readdirSync2,
|
|
1008
|
+
readFileSync as readFileSync3,
|
|
1009
|
+
renameSync as renameSync2,
|
|
1010
|
+
rmSync as rmSync3,
|
|
1011
|
+
statSync as statSync2,
|
|
1012
|
+
unlinkSync,
|
|
1013
|
+
writeFileSync as writeFileSync3
|
|
1014
|
+
} from "node:fs";
|
|
1015
|
+
import { homedir as homedir2 } from "node:os";
|
|
1016
|
+
import { join as join4 } from "node:path";
|
|
1017
|
+
function runStateDir(env = process.env) {
|
|
1018
|
+
const override = env[RUN_STATE_DIR_ENV]?.trim();
|
|
1019
|
+
if (override)
|
|
1020
|
+
return override;
|
|
1021
|
+
return join4(homedir2(), ".harmony", "runs");
|
|
1022
|
+
}
|
|
1023
|
+
function sessionsDir(stateDir) {
|
|
1024
|
+
return join4(stateDir, "sessions");
|
|
1025
|
+
}
|
|
1026
|
+
function spoolDir(stateDir, agentSessionId) {
|
|
1027
|
+
return join4(stateDir, "spool", sanitizeIdForPath(agentSessionId));
|
|
1028
|
+
}
|
|
1029
|
+
function sanitizeIdForPath(id) {
|
|
1030
|
+
return id.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 100);
|
|
1031
|
+
}
|
|
1032
|
+
function pointerFileName(publisherPid, cardId) {
|
|
1033
|
+
return `${publisherPid}.${sanitizeIdForPath(cardId)}.json`;
|
|
1034
|
+
}
|
|
1035
|
+
function pidIsAlive(pid) {
|
|
1036
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
1037
|
+
return false;
|
|
1038
|
+
try {
|
|
1039
|
+
process.kill(pid, 0);
|
|
1040
|
+
return true;
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
return err?.code === "EPERM";
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
function readProcParent(pid) {
|
|
1046
|
+
try {
|
|
1047
|
+
const stat = readFileSync3(`/proc/${pid}/stat`, "utf-8");
|
|
1048
|
+
const tail = stat.slice(stat.lastIndexOf(")") + 1).trim().split(/\s+/);
|
|
1049
|
+
const ppid = Number.parseInt(tail[1] ?? "", 10);
|
|
1050
|
+
return Number.isInteger(ppid) && ppid > 0 ? ppid : null;
|
|
1051
|
+
} catch {
|
|
1052
|
+
return null;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
function psParentTable() {
|
|
1056
|
+
if (psTableCache)
|
|
1057
|
+
return psTableCache;
|
|
1058
|
+
const table = new Map;
|
|
1059
|
+
try {
|
|
1060
|
+
const out = execFileSync("ps", ["-Ao", "pid=,ppid="], {
|
|
1061
|
+
encoding: "utf-8",
|
|
1062
|
+
timeout: 2000,
|
|
1063
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1064
|
+
});
|
|
1065
|
+
for (const line of out.split(`
|
|
1066
|
+
`)) {
|
|
1067
|
+
const match = line.trim().match(/^(\d+)\s+(\d+)$/);
|
|
1068
|
+
if (!match)
|
|
1069
|
+
continue;
|
|
1070
|
+
table.set(Number(match[1]), Number(match[2]));
|
|
1071
|
+
}
|
|
1072
|
+
} catch {}
|
|
1073
|
+
psTableCache = table;
|
|
1074
|
+
return table;
|
|
1075
|
+
}
|
|
1076
|
+
function ancestorPids(pid, readParent) {
|
|
1077
|
+
const parentOf = readParent ?? ((child) => {
|
|
1078
|
+
const viaProc = readProcParent(child);
|
|
1079
|
+
if (viaProc !== null)
|
|
1080
|
+
return viaProc;
|
|
1081
|
+
return psParentTable().get(child) ?? null;
|
|
1082
|
+
});
|
|
1083
|
+
const chain2 = [];
|
|
1084
|
+
const seen = new Set([pid]);
|
|
1085
|
+
let current = pid;
|
|
1086
|
+
if (!readParent && pid === process.pid) {
|
|
1087
|
+
const ppid = process.ppid;
|
|
1088
|
+
if (Number.isInteger(ppid) && ppid > 1) {
|
|
1089
|
+
chain2.push(ppid);
|
|
1090
|
+
seen.add(ppid);
|
|
1091
|
+
current = ppid;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
for (let depth = chain2.length;depth < MAX_ANCESTOR_DEPTH; depth++) {
|
|
1095
|
+
const parent = parentOf(current);
|
|
1096
|
+
if (parent === null || parent <= 1 || seen.has(parent))
|
|
1097
|
+
break;
|
|
1098
|
+
chain2.push(parent);
|
|
1099
|
+
seen.add(parent);
|
|
1100
|
+
current = parent;
|
|
1101
|
+
}
|
|
1102
|
+
return chain2;
|
|
1103
|
+
}
|
|
1104
|
+
function publishRunSession(session, options) {
|
|
1105
|
+
const stateDir = options?.stateDir ?? runStateDir();
|
|
1106
|
+
const pid = options?.pid ?? process.pid;
|
|
1107
|
+
const record = {
|
|
1108
|
+
cardId: session.cardId,
|
|
1109
|
+
agentSessionId: session.agentSessionId,
|
|
1110
|
+
publisherPid: pid,
|
|
1111
|
+
ancestorPids: options?.ancestors ?? ancestorPids(pid),
|
|
1112
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
1113
|
+
updatedAt: new Date().toISOString()
|
|
1114
|
+
};
|
|
1115
|
+
try {
|
|
1116
|
+
const dir = sessionsDir(stateDir);
|
|
1117
|
+
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
1118
|
+
const target = join4(dir, pointerFileName(pid, session.cardId));
|
|
1119
|
+
const temp = `${target}.${process.pid}.tmp`;
|
|
1120
|
+
writeFileSync3(temp, JSON.stringify(record), { mode: 384 });
|
|
1121
|
+
renameSync2(temp, target);
|
|
1122
|
+
return record;
|
|
1123
|
+
} catch {
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
function clearRunSession(cardId, options) {
|
|
1128
|
+
const stateDir = options?.stateDir ?? runStateDir();
|
|
1129
|
+
const pid = options?.pid ?? process.pid;
|
|
1130
|
+
if (!options?.keepPointer) {
|
|
1131
|
+
try {
|
|
1132
|
+
unlinkSync(join4(sessionsDir(stateDir), pointerFileName(pid, cardId)));
|
|
1133
|
+
} catch {}
|
|
1134
|
+
}
|
|
1135
|
+
if (options?.agentSessionId) {
|
|
1136
|
+
try {
|
|
1137
|
+
rmSync3(spoolDir(stateDir, options.agentSessionId), {
|
|
1138
|
+
recursive: true,
|
|
1139
|
+
force: true
|
|
1140
|
+
});
|
|
1141
|
+
} catch {}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
function readPublishedSessions(options) {
|
|
1145
|
+
const stateDir = options?.stateDir ?? runStateDir();
|
|
1146
|
+
const now = options?.now ?? Date.now();
|
|
1147
|
+
const dir = sessionsDir(stateDir);
|
|
1148
|
+
let names;
|
|
1149
|
+
try {
|
|
1150
|
+
names = readdirSync2(dir);
|
|
1151
|
+
} catch {
|
|
1152
|
+
return [];
|
|
1153
|
+
}
|
|
1154
|
+
const live = [];
|
|
1155
|
+
for (const name of names) {
|
|
1156
|
+
if (!name.endsWith(".json"))
|
|
1157
|
+
continue;
|
|
1158
|
+
const path = join4(dir, name);
|
|
1159
|
+
let record;
|
|
1160
|
+
try {
|
|
1161
|
+
record = JSON.parse(readFileSync3(path, "utf-8"));
|
|
1162
|
+
} catch {
|
|
1163
|
+
safeUnlink(path);
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
if (typeof record?.cardId !== "string" || typeof record?.agentSessionId !== "string" || !record.cardId || !record.agentSessionId) {
|
|
1167
|
+
safeUnlink(path);
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
const age = now - Date.parse(record.updatedAt ?? "");
|
|
1171
|
+
if (!Number.isFinite(age) || age > MAX_POINTER_AGE_MS) {
|
|
1172
|
+
safeUnlink(path);
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
if (!pidIsAlive(record.publisherPid)) {
|
|
1176
|
+
safeUnlink(path);
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
live.push({ ...record, ancestorPids: record.ancestorPids ?? [] });
|
|
1180
|
+
}
|
|
1181
|
+
return live;
|
|
1182
|
+
}
|
|
1183
|
+
function safeUnlink(path) {
|
|
1184
|
+
try {
|
|
1185
|
+
unlinkSync(path);
|
|
1186
|
+
} catch {}
|
|
1187
|
+
}
|
|
1188
|
+
function chooseRunSessionForHook(args) {
|
|
1189
|
+
const { candidates, hookAncestorPids } = args;
|
|
1190
|
+
if (candidates.length === 0)
|
|
1191
|
+
return null;
|
|
1192
|
+
const hasCwd = typeof args.cwd === "string" && args.cwd.length > 0;
|
|
1193
|
+
const pool = hasCwd ? candidates.filter((candidate) => candidate.cwd === args.cwd) : candidates;
|
|
1194
|
+
if (pool.length === 0)
|
|
1195
|
+
return null;
|
|
1196
|
+
let best = null;
|
|
1197
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
1198
|
+
for (const candidate of pool) {
|
|
1199
|
+
const claimed = new Set([
|
|
1200
|
+
candidate.publisherPid,
|
|
1201
|
+
...candidate.ancestorPids ?? []
|
|
1202
|
+
]);
|
|
1203
|
+
let score = Number.POSITIVE_INFINITY;
|
|
1204
|
+
for (let i = 0;i < hookAncestorPids.length; i++) {
|
|
1205
|
+
if (claimed.has(hookAncestorPids[i])) {
|
|
1206
|
+
score = i;
|
|
1207
|
+
break;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
if (score === Number.POSITIVE_INFINITY)
|
|
1211
|
+
continue;
|
|
1212
|
+
if (score < bestScore || score === bestScore && best !== null && Date.parse(candidate.updatedAt) > Date.parse(best.updatedAt)) {
|
|
1213
|
+
best = candidate;
|
|
1214
|
+
bestScore = score;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
if (best)
|
|
1218
|
+
return best;
|
|
1219
|
+
if (hasCwd && pool.length === 1)
|
|
1220
|
+
return pool[0];
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
function writeSpoolBatch(dir, events, options) {
|
|
1224
|
+
if (events.length === 0)
|
|
1225
|
+
return null;
|
|
1226
|
+
const now = options?.now ?? Date.now();
|
|
1227
|
+
const pid = options?.pid ?? process.pid;
|
|
1228
|
+
const nonce = options?.nonce ?? Math.random().toString(36).slice(2, 8).padEnd(6, "0");
|
|
1229
|
+
try {
|
|
1230
|
+
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
1231
|
+
const name = `${String(now).padStart(14, "0")}-${pid}-${nonce}.json`;
|
|
1232
|
+
const target = join4(dir, name);
|
|
1233
|
+
const temp = `${target}.tmp`;
|
|
1234
|
+
writeFileSync3(temp, JSON.stringify(events), { mode: 384 });
|
|
1235
|
+
renameSync2(temp, target);
|
|
1236
|
+
return target;
|
|
1237
|
+
} catch {
|
|
1238
|
+
return null;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
function readSpoolBatches(dir, limit = 200) {
|
|
1242
|
+
let names;
|
|
1243
|
+
try {
|
|
1244
|
+
names = readdirSync2(dir);
|
|
1245
|
+
} catch {
|
|
1246
|
+
return [];
|
|
1247
|
+
}
|
|
1248
|
+
const batches = [];
|
|
1249
|
+
for (const name of names.filter((n) => n.endsWith(".json")).sort()) {
|
|
1250
|
+
if (batches.length >= limit)
|
|
1251
|
+
break;
|
|
1252
|
+
const path = join4(dir, name);
|
|
1253
|
+
try {
|
|
1254
|
+
const parsed = JSON.parse(readFileSync3(path, "utf-8"));
|
|
1255
|
+
if (!Array.isArray(parsed)) {
|
|
1256
|
+
safeUnlink(path);
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
batches.push({ path, events: parsed });
|
|
1260
|
+
} catch {
|
|
1261
|
+
safeUnlink(path);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return batches;
|
|
1265
|
+
}
|
|
1266
|
+
function removeSpoolBatches(paths) {
|
|
1267
|
+
for (const path of paths)
|
|
1268
|
+
safeUnlink(path);
|
|
1269
|
+
}
|
|
1270
|
+
function trimSpool(dir, max) {
|
|
1271
|
+
let names;
|
|
1272
|
+
try {
|
|
1273
|
+
names = readdirSync2(dir).filter((n) => n.endsWith(".json")).sort();
|
|
1274
|
+
} catch {
|
|
1275
|
+
return 0;
|
|
1276
|
+
}
|
|
1277
|
+
if (names.length <= max)
|
|
1278
|
+
return 0;
|
|
1279
|
+
const excess = names.slice(0, names.length - max);
|
|
1280
|
+
for (const name of excess)
|
|
1281
|
+
safeUnlink(join4(dir, name));
|
|
1282
|
+
return excess.length;
|
|
1283
|
+
}
|
|
1284
|
+
function routeMemoPath(stateDir, harnessSessionId) {
|
|
1285
|
+
return join4(stateDir, "routes", `${sanitizeIdForPath(harnessSessionId)}.json`);
|
|
1286
|
+
}
|
|
1287
|
+
function readRouteMemo(stateDir, harnessSessionId) {
|
|
1288
|
+
try {
|
|
1289
|
+
const raw = JSON.parse(readFileSync3(routeMemoPath(stateDir, harnessSessionId), "utf-8"));
|
|
1290
|
+
const pid = raw?.publisherPid;
|
|
1291
|
+
const cardId = raw?.cardId;
|
|
1292
|
+
const agentSessionId = raw?.agentSessionId;
|
|
1293
|
+
if (typeof pid !== "number" || !Number.isInteger(pid))
|
|
1294
|
+
return null;
|
|
1295
|
+
if (typeof cardId !== "string" || cardId.length === 0)
|
|
1296
|
+
return null;
|
|
1297
|
+
if (typeof agentSessionId !== "string" || agentSessionId.length === 0) {
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
return { publisherPid: pid, cardId, agentSessionId };
|
|
1301
|
+
} catch {
|
|
1302
|
+
return null;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
function writeRouteMemo(stateDir, harnessSessionId, memo) {
|
|
1306
|
+
try {
|
|
1307
|
+
const path = routeMemoPath(stateDir, harnessSessionId);
|
|
1308
|
+
mkdirSync3(join4(stateDir, "routes"), { recursive: true, mode: 448 });
|
|
1309
|
+
writeFileSync3(path, JSON.stringify({
|
|
1310
|
+
publisherPid: memo.publisherPid,
|
|
1311
|
+
cardId: memo.cardId,
|
|
1312
|
+
agentSessionId: memo.agentSessionId
|
|
1313
|
+
}), { mode: 384 });
|
|
1314
|
+
} catch {}
|
|
1315
|
+
}
|
|
1316
|
+
function runStateExists(stateDir = runStateDir()) {
|
|
1317
|
+
try {
|
|
1318
|
+
return existsSync3(sessionsDir(stateDir)) && statSync2(sessionsDir(stateDir)).isDirectory();
|
|
1319
|
+
} catch {
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
var RUN_STATE_DIR_ENV = "HARMONY_RUN_STATE_DIR", MAX_POINTER_AGE_MS, MAX_ANCESTOR_DEPTH = 12, psTableCache = null;
|
|
1324
|
+
var init_run_state = __esm(() => {
|
|
1325
|
+
MAX_POINTER_AGE_MS = 10 * 60000;
|
|
1326
|
+
});
|
|
1327
|
+
|
|
981
1328
|
// src/server.ts
|
|
982
1329
|
import { createHash as createHash4 } from "node:crypto";
|
|
983
1330
|
import { readFile } from "node:fs/promises";
|
|
@@ -2641,6 +2988,222 @@ function resetClient() {
|
|
|
2641
2988
|
client2 = null;
|
|
2642
2989
|
}
|
|
2643
2990
|
|
|
2991
|
+
// src/comment-session.ts
|
|
2992
|
+
var RUN_SESSION_CARD_ENV = "HARMONY_AGENT_CARD_ID";
|
|
2993
|
+
var RUN_SESSION_ID_ENV = "HARMONY_AGENT_SESSION_ID";
|
|
2994
|
+
function readDeclaredRunSession(env = process.env) {
|
|
2995
|
+
const cardId = env[RUN_SESSION_CARD_ENV]?.trim();
|
|
2996
|
+
const agentSessionId = env[RUN_SESSION_ID_ENV]?.trim();
|
|
2997
|
+
if (!cardId || !agentSessionId)
|
|
2998
|
+
return null;
|
|
2999
|
+
return { cardId, agentSessionId };
|
|
3000
|
+
}
|
|
3001
|
+
function chooseCommentSession(args) {
|
|
3002
|
+
const tracked = args.tracked;
|
|
3003
|
+
if (tracked?.agentSessionId && tracked.scopeId === args.callerScopeId) {
|
|
3004
|
+
return {
|
|
3005
|
+
kind: "session",
|
|
3006
|
+
agentSessionId: tracked.agentSessionId,
|
|
3007
|
+
source: "tracked"
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
const declared = args.declared;
|
|
3011
|
+
if (declared && declared.cardId === args.cardId) {
|
|
3012
|
+
return {
|
|
3013
|
+
kind: "session",
|
|
3014
|
+
agentSessionId: declared.agentSessionId,
|
|
3015
|
+
source: "declared"
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
return { kind: "sessionless" };
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
// src/run-event-forwarder.ts
|
|
3022
|
+
init_run_state();
|
|
3023
|
+
var FLUSH_INTERVAL_MS = 2000;
|
|
3024
|
+
var MAX_INTERVAL_MS = 60000;
|
|
3025
|
+
var MAX_EVENTS_PER_REQUEST = 500;
|
|
3026
|
+
var MAX_BATCHES_PER_FLUSH = 200;
|
|
3027
|
+
var MAX_BATCH_ATTEMPTS = 20;
|
|
3028
|
+
var POINTER_REFRESH_MS = 60000;
|
|
3029
|
+
|
|
3030
|
+
class RunEventForwarder {
|
|
3031
|
+
cardId;
|
|
3032
|
+
agentSessionId;
|
|
3033
|
+
getClient;
|
|
3034
|
+
stateDir;
|
|
3035
|
+
baseIntervalMs;
|
|
3036
|
+
timer = null;
|
|
3037
|
+
flushing = false;
|
|
3038
|
+
stopped = false;
|
|
3039
|
+
consecutiveFailures = 0;
|
|
3040
|
+
lastPointerRefresh = Date.now();
|
|
3041
|
+
attempts = new Map;
|
|
3042
|
+
constructor(options) {
|
|
3043
|
+
this.cardId = options.cardId;
|
|
3044
|
+
this.agentSessionId = options.agentSessionId;
|
|
3045
|
+
this.getClient = options.getClient;
|
|
3046
|
+
this.stateDir = options.stateDir ?? runStateDir();
|
|
3047
|
+
this.baseIntervalMs = options.intervalMs ?? FLUSH_INTERVAL_MS;
|
|
3048
|
+
}
|
|
3049
|
+
get dir() {
|
|
3050
|
+
return spoolDir(this.stateDir, this.agentSessionId);
|
|
3051
|
+
}
|
|
3052
|
+
nextDelay() {
|
|
3053
|
+
if (this.consecutiveFailures === 0)
|
|
3054
|
+
return this.baseIntervalMs;
|
|
3055
|
+
const scaled = this.baseIntervalMs * 2 ** Math.min(this.consecutiveFailures, 6);
|
|
3056
|
+
return Math.min(scaled, MAX_INTERVAL_MS);
|
|
3057
|
+
}
|
|
3058
|
+
start() {
|
|
3059
|
+
if (this.timer || this.stopped)
|
|
3060
|
+
return;
|
|
3061
|
+
this.schedule();
|
|
3062
|
+
}
|
|
3063
|
+
schedule() {
|
|
3064
|
+
if (this.stopped)
|
|
3065
|
+
return;
|
|
3066
|
+
this.timer = setTimeout(() => {
|
|
3067
|
+
this.timer = null;
|
|
3068
|
+
this.flush().finally(() => this.schedule());
|
|
3069
|
+
}, this.nextDelay());
|
|
3070
|
+
this.timer.unref?.();
|
|
3071
|
+
}
|
|
3072
|
+
async flush() {
|
|
3073
|
+
if (this.flushing)
|
|
3074
|
+
return 0;
|
|
3075
|
+
this.flushing = true;
|
|
3076
|
+
try {
|
|
3077
|
+
this.refreshPointer();
|
|
3078
|
+
const batches = readSpoolBatches(this.dir, MAX_BATCHES_PER_FLUSH);
|
|
3079
|
+
if (batches.length === 0) {
|
|
3080
|
+
this.consecutiveFailures = 0;
|
|
3081
|
+
return 0;
|
|
3082
|
+
}
|
|
3083
|
+
const paths = [];
|
|
3084
|
+
const events = [];
|
|
3085
|
+
for (const batch of batches) {
|
|
3086
|
+
if (events.length > 0 && events.length + batch.events.length > MAX_EVENTS_PER_REQUEST) {
|
|
3087
|
+
break;
|
|
3088
|
+
}
|
|
3089
|
+
paths.push(batch.path);
|
|
3090
|
+
events.push(...batch.events);
|
|
3091
|
+
}
|
|
3092
|
+
if (events.length === 0)
|
|
3093
|
+
return 0;
|
|
3094
|
+
try {
|
|
3095
|
+
await this.getClient().appendAgentRunEvents(this.cardId, {
|
|
3096
|
+
sessionId: this.agentSessionId,
|
|
3097
|
+
events
|
|
3098
|
+
});
|
|
3099
|
+
removeSpoolBatches(paths);
|
|
3100
|
+
for (const path of paths)
|
|
3101
|
+
this.attempts.delete(path);
|
|
3102
|
+
this.consecutiveFailures = 0;
|
|
3103
|
+
return events.length;
|
|
3104
|
+
} catch {
|
|
3105
|
+
this.consecutiveFailures++;
|
|
3106
|
+
const poisoned = [];
|
|
3107
|
+
for (const path of paths) {
|
|
3108
|
+
const next = (this.attempts.get(path) ?? 0) + 1;
|
|
3109
|
+
this.attempts.set(path, next);
|
|
3110
|
+
if (next >= MAX_BATCH_ATTEMPTS)
|
|
3111
|
+
poisoned.push(path);
|
|
3112
|
+
}
|
|
3113
|
+
if (poisoned.length > 0) {
|
|
3114
|
+
removeSpoolBatches(poisoned);
|
|
3115
|
+
for (const path of poisoned)
|
|
3116
|
+
this.attempts.delete(path);
|
|
3117
|
+
}
|
|
3118
|
+
return 0;
|
|
3119
|
+
}
|
|
3120
|
+
} finally {
|
|
3121
|
+
this.flushing = false;
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
refreshPointer() {
|
|
3125
|
+
const now = Date.now();
|
|
3126
|
+
if (now - this.lastPointerRefresh < POINTER_REFRESH_MS)
|
|
3127
|
+
return;
|
|
3128
|
+
this.lastPointerRefresh = now;
|
|
3129
|
+
publishRunSession({ cardId: this.cardId, agentSessionId: this.agentSessionId }, { stateDir: this.stateDir });
|
|
3130
|
+
}
|
|
3131
|
+
async stop(handover) {
|
|
3132
|
+
if (this.stopped)
|
|
3133
|
+
return;
|
|
3134
|
+
this.stopped = true;
|
|
3135
|
+
if (this.timer) {
|
|
3136
|
+
clearTimeout(this.timer);
|
|
3137
|
+
this.timer = null;
|
|
3138
|
+
}
|
|
3139
|
+
try {
|
|
3140
|
+
await this.flush();
|
|
3141
|
+
} catch {}
|
|
3142
|
+
const handoverTo = handover?.handoverTo;
|
|
3143
|
+
const keepPointer = handoverTo !== undefined;
|
|
3144
|
+
const keepSpool = handoverTo === this.agentSessionId;
|
|
3145
|
+
clearRunSession(this.cardId, {
|
|
3146
|
+
stateDir: this.stateDir,
|
|
3147
|
+
...keepSpool ? {} : { agentSessionId: this.agentSessionId },
|
|
3148
|
+
...keepPointer ? { keepPointer: true } : {}
|
|
3149
|
+
});
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
var forwarders = new Map;
|
|
3153
|
+
var DISABLE_RUN_HOOK_ENV = "HARMONY_DISABLE_RUN_HOOK";
|
|
3154
|
+
function hookTimelineDisabled(env, explicitStateDir) {
|
|
3155
|
+
const off = env[DISABLE_RUN_HOOK_ENV]?.trim().toLowerCase();
|
|
3156
|
+
if (off && off !== "0" && off !== "false")
|
|
3157
|
+
return true;
|
|
3158
|
+
if (env.NODE_ENV === "test" && !explicitStateDir)
|
|
3159
|
+
return true;
|
|
3160
|
+
return false;
|
|
3161
|
+
}
|
|
3162
|
+
function startRunEventForwarder(options) {
|
|
3163
|
+
const existing = forwarders.get(options.cardId);
|
|
3164
|
+
if (existing)
|
|
3165
|
+
existing.stop({ handoverTo: options.agentSessionId });
|
|
3166
|
+
const forwarder = new RunEventForwarder(options);
|
|
3167
|
+
forwarders.set(options.cardId, forwarder);
|
|
3168
|
+
forwarder.start();
|
|
3169
|
+
return forwarder;
|
|
3170
|
+
}
|
|
3171
|
+
async function stopRunEventForwarder(cardId) {
|
|
3172
|
+
const forwarder = forwarders.get(cardId);
|
|
3173
|
+
if (!forwarder)
|
|
3174
|
+
return;
|
|
3175
|
+
forwarders.delete(cardId);
|
|
3176
|
+
await forwarder.stop();
|
|
3177
|
+
}
|
|
3178
|
+
async function stopAllRunEventForwarders() {
|
|
3179
|
+
const all = [...forwarders.values()];
|
|
3180
|
+
forwarders.clear();
|
|
3181
|
+
await Promise.all(all.map((f) => f.stop().catch(() => {
|
|
3182
|
+
return;
|
|
3183
|
+
})));
|
|
3184
|
+
}
|
|
3185
|
+
function beginHookTimeline(args) {
|
|
3186
|
+
if (!args.agentSessionId)
|
|
3187
|
+
return null;
|
|
3188
|
+
const env = args.env ?? process.env;
|
|
3189
|
+
if (readDeclaredRunSession(env))
|
|
3190
|
+
return null;
|
|
3191
|
+
if (hookTimelineDisabled(env, args.stateDir))
|
|
3192
|
+
return null;
|
|
3193
|
+
const published = publishRunSession({ cardId: args.cardId, agentSessionId: args.agentSessionId }, args.stateDir ? { stateDir: args.stateDir } : undefined);
|
|
3194
|
+
if (!published)
|
|
3195
|
+
return null;
|
|
3196
|
+
return startRunEventForwarder({
|
|
3197
|
+
cardId: args.cardId,
|
|
3198
|
+
agentSessionId: args.agentSessionId,
|
|
3199
|
+
getClient: args.getClient,
|
|
3200
|
+
...args.stateDir ? { stateDir: args.stateDir } : {}
|
|
3201
|
+
});
|
|
3202
|
+
}
|
|
3203
|
+
async function endHookTimeline(cardId) {
|
|
3204
|
+
await stopRunEventForwarder(cardId);
|
|
3205
|
+
}
|
|
3206
|
+
|
|
2644
3207
|
// src/auto-session.ts
|
|
2645
3208
|
var CLIENT_DISPLAY_NAMES = {
|
|
2646
3209
|
"claude-code": "Claude Code",
|
|
@@ -2724,6 +3287,7 @@ async function trackActivity(cardId, options) {
|
|
|
2724
3287
|
for (const otherCardId of toEnd) {
|
|
2725
3288
|
await autoEndSession(scope, client3, otherCardId, "completed");
|
|
2726
3289
|
}
|
|
3290
|
+
let agentSessionId;
|
|
2727
3291
|
try {
|
|
2728
3292
|
const started = await client3.startAgentSession(cardId, {
|
|
2729
3293
|
agentIdentifier,
|
|
@@ -2733,6 +3297,7 @@ async function trackActivity(cardId, options) {
|
|
|
2733
3297
|
});
|
|
2734
3298
|
if (started?.session === null)
|
|
2735
3299
|
return;
|
|
3300
|
+
agentSessionId = started?.session?.id;
|
|
2736
3301
|
} catch {}
|
|
2737
3302
|
scope.sessions.set(cardId, {
|
|
2738
3303
|
cardId,
|
|
@@ -2741,8 +3306,14 @@ async function trackActivity(cardId, options) {
|
|
|
2741
3306
|
isExplicit: false,
|
|
2742
3307
|
agentIdentifier,
|
|
2743
3308
|
agentName,
|
|
3309
|
+
agentSessionId,
|
|
2744
3310
|
status: "working"
|
|
2745
3311
|
});
|
|
3312
|
+
beginHookTimeline({
|
|
3313
|
+
cardId,
|
|
3314
|
+
agentSessionId,
|
|
3315
|
+
getClient: () => client3
|
|
3316
|
+
});
|
|
2746
3317
|
}
|
|
2747
3318
|
function markExplicit(cardId, options) {
|
|
2748
3319
|
const scope = getOrCreateScope(options?.scopeId ?? DEFAULT_SCOPE);
|
|
@@ -2843,6 +3414,9 @@ function sweepTick() {
|
|
|
2843
3414
|
async function autoEndSession(scope, client3, cardId, status) {
|
|
2844
3415
|
if (!scope.sessions.delete(cardId))
|
|
2845
3416
|
return;
|
|
3417
|
+
try {
|
|
3418
|
+
await endHookTimeline(cardId);
|
|
3419
|
+
} catch {}
|
|
2846
3420
|
try {
|
|
2847
3421
|
await client3.endAgentSession(cardId, { status });
|
|
2848
3422
|
} catch {}
|
|
@@ -2851,36 +3425,6 @@ async function autoEndSession(scope, client3, cardId, status) {
|
|
|
2851
3425
|
} catch {}
|
|
2852
3426
|
}
|
|
2853
3427
|
|
|
2854
|
-
// src/comment-session.ts
|
|
2855
|
-
var RUN_SESSION_CARD_ENV = "HARMONY_AGENT_CARD_ID";
|
|
2856
|
-
var RUN_SESSION_ID_ENV = "HARMONY_AGENT_SESSION_ID";
|
|
2857
|
-
function readDeclaredRunSession(env = process.env) {
|
|
2858
|
-
const cardId = env[RUN_SESSION_CARD_ENV]?.trim();
|
|
2859
|
-
const agentSessionId = env[RUN_SESSION_ID_ENV]?.trim();
|
|
2860
|
-
if (!cardId || !agentSessionId)
|
|
2861
|
-
return null;
|
|
2862
|
-
return { cardId, agentSessionId };
|
|
2863
|
-
}
|
|
2864
|
-
function chooseCommentSession(args) {
|
|
2865
|
-
const tracked = args.tracked;
|
|
2866
|
-
if (tracked?.agentSessionId && tracked.scopeId === args.callerScopeId) {
|
|
2867
|
-
return {
|
|
2868
|
-
kind: "session",
|
|
2869
|
-
agentSessionId: tracked.agentSessionId,
|
|
2870
|
-
source: "tracked"
|
|
2871
|
-
};
|
|
2872
|
-
}
|
|
2873
|
-
const declared = args.declared;
|
|
2874
|
-
if (declared && declared.cardId === args.cardId) {
|
|
2875
|
-
return {
|
|
2876
|
-
kind: "session",
|
|
2877
|
-
agentSessionId: declared.agentSessionId,
|
|
2878
|
-
source: "declared"
|
|
2879
|
-
};
|
|
2880
|
-
}
|
|
2881
|
-
return { kind: "sessionless" };
|
|
2882
|
-
}
|
|
2883
|
-
|
|
2884
3428
|
// src/server.ts
|
|
2885
3429
|
init_config();
|
|
2886
3430
|
|
|
@@ -3554,30 +4098,30 @@ async function collectPlaybookMetricWarnings(client3, workspaceId, steps) {
|
|
|
3554
4098
|
|
|
3555
4099
|
// src/skills.ts
|
|
3556
4100
|
import {
|
|
3557
|
-
existsSync as
|
|
3558
|
-
mkdirSync as
|
|
3559
|
-
readFileSync as
|
|
3560
|
-
renameSync as
|
|
3561
|
-
writeFileSync as
|
|
4101
|
+
existsSync as existsSync5,
|
|
4102
|
+
mkdirSync as mkdirSync4,
|
|
4103
|
+
readFileSync as readFileSync5,
|
|
4104
|
+
renameSync as renameSync3,
|
|
4105
|
+
writeFileSync as writeFileSync4
|
|
3562
4106
|
} from "node:fs";
|
|
3563
|
-
import { homedir as
|
|
3564
|
-
import { dirname as dirname2, join as
|
|
4107
|
+
import { homedir as homedir4 } from "node:os";
|
|
4108
|
+
import { dirname as dirname2, join as join6 } from "node:path";
|
|
3565
4109
|
init_config();
|
|
3566
4110
|
|
|
3567
4111
|
// src/hmy-config.ts
|
|
3568
|
-
import { existsSync as
|
|
3569
|
-
import { homedir as
|
|
3570
|
-
import { join as
|
|
4112
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
|
|
4113
|
+
import { homedir as homedir3 } from "node:os";
|
|
4114
|
+
import { join as join5 } from "node:path";
|
|
3571
4115
|
var DEFAULTS = { updateCheck: true, pin: null };
|
|
3572
4116
|
function getHmyConfigPath() {
|
|
3573
|
-
return
|
|
4117
|
+
return join5(homedir3(), ".hmy", "config.yaml");
|
|
3574
4118
|
}
|
|
3575
4119
|
function loadHmyConfig() {
|
|
3576
4120
|
const path = getHmyConfigPath();
|
|
3577
|
-
if (!
|
|
4121
|
+
if (!existsSync4(path))
|
|
3578
4122
|
return { ...DEFAULTS };
|
|
3579
4123
|
try {
|
|
3580
|
-
return parseHmyConfig(
|
|
4124
|
+
return parseHmyConfig(readFileSync4(path, "utf-8"));
|
|
3581
4125
|
} catch {
|
|
3582
4126
|
return { ...DEFAULTS };
|
|
3583
4127
|
}
|
|
@@ -3709,11 +4253,11 @@ function stripSkillPreamble(content) {
|
|
|
3709
4253
|
}
|
|
3710
4254
|
function atomicWrite(filePath, content) {
|
|
3711
4255
|
const dir = dirname2(filePath);
|
|
3712
|
-
if (!
|
|
3713
|
-
|
|
4256
|
+
if (!existsSync5(dir))
|
|
4257
|
+
mkdirSync4(dir, { recursive: true });
|
|
3714
4258
|
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
3715
|
-
|
|
3716
|
-
|
|
4259
|
+
writeFileSync4(tmp, content);
|
|
4260
|
+
renameSync3(tmp, filePath);
|
|
3717
4261
|
}
|
|
3718
4262
|
function hasMetadataVersion(content) {
|
|
3719
4263
|
return parseSkillVersion(content) !== null;
|
|
@@ -3742,7 +4286,7 @@ function parseSkillVersion(content) {
|
|
|
3742
4286
|
function findSkillFiles(paths, knownNames) {
|
|
3743
4287
|
const results = [];
|
|
3744
4288
|
for (const filePath of paths) {
|
|
3745
|
-
if (!
|
|
4289
|
+
if (!existsSync5(filePath))
|
|
3746
4290
|
continue;
|
|
3747
4291
|
for (const name of knownNames) {
|
|
3748
4292
|
if (filePath.includes(`/${name}/`) || filePath.includes(`/${name}.md`)) {
|
|
@@ -3753,15 +4297,15 @@ function findSkillFiles(paths, knownNames) {
|
|
|
3753
4297
|
}
|
|
3754
4298
|
return results;
|
|
3755
4299
|
}
|
|
3756
|
-
var HMY_DIR =
|
|
3757
|
-
var HMY_VERSION_FILE =
|
|
3758
|
-
var LAST_CHECK_FILE =
|
|
4300
|
+
var HMY_DIR = join6(homedir4(), ".hmy");
|
|
4301
|
+
var HMY_VERSION_FILE = join6(HMY_DIR, "VERSION");
|
|
4302
|
+
var LAST_CHECK_FILE = join6(HMY_DIR, "last-update-check");
|
|
3759
4303
|
var CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
3760
4304
|
function checkedRecently(now = Date.now()) {
|
|
3761
4305
|
try {
|
|
3762
|
-
if (!
|
|
4306
|
+
if (!existsSync5(LAST_CHECK_FILE))
|
|
3763
4307
|
return false;
|
|
3764
|
-
const ts = Number.parseInt(
|
|
4308
|
+
const ts = Number.parseInt(readFileSync5(LAST_CHECK_FILE, "utf-8").trim(), 10);
|
|
3765
4309
|
if (!Number.isFinite(ts))
|
|
3766
4310
|
return false;
|
|
3767
4311
|
return now - ts < CHECK_TTL_MS;
|
|
@@ -3771,9 +4315,9 @@ function checkedRecently(now = Date.now()) {
|
|
|
3771
4315
|
}
|
|
3772
4316
|
function recordCheck(now = Date.now()) {
|
|
3773
4317
|
try {
|
|
3774
|
-
if (!
|
|
3775
|
-
|
|
3776
|
-
|
|
4318
|
+
if (!existsSync5(HMY_DIR))
|
|
4319
|
+
mkdirSync4(HMY_DIR, { recursive: true });
|
|
4320
|
+
writeFileSync4(LAST_CHECK_FILE, String(now));
|
|
3777
4321
|
} catch {}
|
|
3778
4322
|
}
|
|
3779
4323
|
async function refreshSkills(opts = {}) {
|
|
@@ -3805,7 +4349,7 @@ async function refreshSkills(opts = {}) {
|
|
|
3805
4349
|
const parentDir = dirname2(samplePath);
|
|
3806
4350
|
siblingPath = `${parentDir}/${name}.md`;
|
|
3807
4351
|
}
|
|
3808
|
-
if (
|
|
4352
|
+
if (existsSync5(siblingPath)) {
|
|
3809
4353
|
skillFiles.push({ name, filePath: siblingPath });
|
|
3810
4354
|
}
|
|
3811
4355
|
}
|
|
@@ -3815,7 +4359,7 @@ async function refreshSkills(opts = {}) {
|
|
|
3815
4359
|
let updated = false;
|
|
3816
4360
|
for (const { name, filePath } of skillFiles) {
|
|
3817
4361
|
try {
|
|
3818
|
-
const currentContent =
|
|
4362
|
+
const currentContent = readFileSync5(filePath, "utf-8");
|
|
3819
4363
|
const localVersion = parseSkillVersion(currentContent);
|
|
3820
4364
|
const fetched = await client3.fetchSkill(name);
|
|
3821
4365
|
const remoteVersion = fetched.skillVersion;
|
|
@@ -5621,7 +6165,7 @@ var TOOLS = {
|
|
|
5621
6165
|
}
|
|
5622
6166
|
},
|
|
5623
6167
|
harmony_link_plan_task: {
|
|
5624
|
-
description: "Point a plan CRITERION at the card that fulfils it, and/or set the criterion's status. " + "A plan task is a success criterion, not a work item: `cards.plan_id` says which plan a card " + "belongs to, and this sets the return leg the plan needs to show real progress instead of " + "guessing from board-column names. Use it to repair a link, to re-point a criterion at a " + "different card, or to mark a criterion `completed` once its card actually delivered it. " + "Leave a criterion the card did NOT deliver open — an open criterion is the signal.",
|
|
6168
|
+
description: "Point a plan CRITERION at the card that fulfils it, and/or set the criterion's status. " + "A plan task is a success criterion, not a work item: `cards.plan_id` says which plan a card " + "belongs to, and this sets the return leg the plan needs to show real progress instead of " + "guessing from board-column names. Use it to repair a link, to re-point a criterion at a " + "different card, or to mark a criterion `completed` once its card actually delivered it. " + "Leave a criterion the card did NOT deliver open — an open criterion is the signal. " + "Linking a card that belongs to no plan yet also adopts it into this one (reported as " + "`cardPlanAdopted`), and linking a card that already belongs to a DIFFERENT plan is " + "refused — move it with harmony_update_card first, so the two directions cannot drift.",
|
|
5625
6169
|
inputSchema: {
|
|
5626
6170
|
type: "object",
|
|
5627
6171
|
properties: {
|
|
@@ -5632,7 +6176,7 @@ var TOOLS = {
|
|
|
5632
6176
|
},
|
|
5633
6177
|
cardId: {
|
|
5634
6178
|
type: "string",
|
|
5635
|
-
description: "Card that fulfils this criterion. Must live in the plan's own project."
|
|
6179
|
+
description: "Card that fulfils this criterion. Must live in the plan's own project, and " + "must belong to this plan or to no plan yet — a card already in another plan " + "is refused rather than silently re-pointed."
|
|
5636
6180
|
},
|
|
5637
6181
|
status: {
|
|
5638
6182
|
type: "string",
|
|
@@ -6836,6 +7380,11 @@ ${options}
|
|
|
6836
7380
|
});
|
|
6837
7381
|
const agentSessionId = result.session?.id;
|
|
6838
7382
|
initMemorySession(cardId, agentIdentifier, agentName, agentSessionId, deps.getScopeId?.());
|
|
7383
|
+
beginHookTimeline({
|
|
7384
|
+
cardId,
|
|
7385
|
+
agentSessionId,
|
|
7386
|
+
getClient: () => client3
|
|
7387
|
+
});
|
|
6839
7388
|
return {
|
|
6840
7389
|
success: true,
|
|
6841
7390
|
assignedTo,
|
|
@@ -6897,6 +7446,7 @@ ${options}
|
|
|
6897
7446
|
const endProgressPercent = optionalPercentArg(args.progressPercent, "progressPercent");
|
|
6898
7447
|
await flushMemoryActions(client3, cardId);
|
|
6899
7448
|
cleanupMemorySession(cardId);
|
|
7449
|
+
await endHookTimeline(cardId);
|
|
6900
7450
|
let result = {
|
|
6901
7451
|
session: null
|
|
6902
7452
|
};
|
|
@@ -7506,7 +8056,7 @@ ${options}
|
|
|
7506
8056
|
} else if (args.cardId) {
|
|
7507
8057
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
7508
8058
|
result = await client3.getPlanByCardId(cardId);
|
|
7509
|
-
if (!result) {
|
|
8059
|
+
if (!result || result.plan == null && !result.foreign_criteria?.length) {
|
|
7510
8060
|
return {
|
|
7511
8061
|
success: true,
|
|
7512
8062
|
plan: null,
|
|
@@ -7514,13 +8064,23 @@ ${options}
|
|
|
7514
8064
|
message: "No plan linked to this card"
|
|
7515
8065
|
};
|
|
7516
8066
|
}
|
|
8067
|
+
if (result.plan == null && result.foreign_criteria?.length) {
|
|
8068
|
+
return {
|
|
8069
|
+
success: true,
|
|
8070
|
+
plan: null,
|
|
8071
|
+
tasks: [],
|
|
8072
|
+
divergentCriteria: result.foreign_criteria ?? [],
|
|
8073
|
+
message: "This card belongs to no plan, but plan criteria point at it. " + "Repair it with harmony_update_card (planId) or harmony_link_plan_task."
|
|
8074
|
+
};
|
|
8075
|
+
}
|
|
7517
8076
|
} else {
|
|
7518
8077
|
throw new Error("Either planId or cardId must be provided");
|
|
7519
8078
|
}
|
|
7520
8079
|
return {
|
|
7521
8080
|
success: true,
|
|
7522
8081
|
plan: result.plan,
|
|
7523
|
-
tasks: result.tasks
|
|
8082
|
+
tasks: result.tasks,
|
|
8083
|
+
...result.foreign_criteria?.length ? { divergentCriteria: result.foreign_criteria } : {}
|
|
7524
8084
|
};
|
|
7525
8085
|
}
|
|
7526
8086
|
case "harmony_update_plan": {
|
|
@@ -7548,7 +8108,10 @@ ${options}
|
|
|
7548
8108
|
const found = findPlanTask(tasks, taskId);
|
|
7549
8109
|
if (!found.ok)
|
|
7550
8110
|
throw new Error(found.reason);
|
|
7551
|
-
await client3.updatePlanTask(planId, taskId, {
|
|
8111
|
+
const result = await client3.updatePlanTask(planId, taskId, {
|
|
8112
|
+
cardId,
|
|
8113
|
+
status
|
|
8114
|
+
});
|
|
7552
8115
|
return {
|
|
7553
8116
|
success: true,
|
|
7554
8117
|
planTask: {
|
|
@@ -7556,6 +8119,7 @@ ${options}
|
|
|
7556
8119
|
taskId,
|
|
7557
8120
|
criterion: found.task.content ?? null,
|
|
7558
8121
|
...cardId ? linkedReport(planId, found.task, cardId) : { linked: found.task.card_id != null },
|
|
8122
|
+
...cardId && result.cardPlanAdopted ? { cardPlanAdopted: true } : {},
|
|
7559
8123
|
...status ? { status } : {}
|
|
7560
8124
|
}
|
|
7561
8125
|
};
|
|
@@ -7790,6 +8354,9 @@ class HarmonyMCPServer {
|
|
|
7790
8354
|
try {
|
|
7791
8355
|
await shutdownAllSessions();
|
|
7792
8356
|
} catch {}
|
|
8357
|
+
try {
|
|
8358
|
+
await stopAllRunEventForwarders();
|
|
8359
|
+
} catch {}
|
|
7793
8360
|
destroyAutoSession();
|
|
7794
8361
|
process.exit(exitCode);
|
|
7795
8362
|
};
|