@gethmy/mcp 3.2.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 +41 -2
- package/dist/cli.js +1188 -157
- package/dist/index.js +805 -42
- package/dist/lib/api-client.js +3 -1
- package/dist/run-hook-cli.js +742 -0
- package/package.json +4 -3
- package/src/api-client.ts +57 -1
- package/src/auto-session.ts +33 -0
- package/src/cli.ts +104 -0
- package/src/comment-session.ts +149 -0
- package/src/hook-install.ts +388 -0
- package/src/plan-task-link.ts +130 -0
- 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 +342 -34
- package/src/tui/setup.ts +3 -0
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";
|
|
@@ -1384,7 +1731,6 @@ var AGENT_MILESTONE_LIVENESS_MS = 30 * 60 * 1000;
|
|
|
1384
1731
|
var AGENT_SWEEP_DAEMON_MS = 30 * 60 * 1000;
|
|
1385
1732
|
var AGENT_SWEEP_INTERACTIVE_MS = 2 * 60 * 60 * 1000;
|
|
1386
1733
|
var AGENT_SWEEP_PAUSED_MS = 4 * 60 * 60 * 1000;
|
|
1387
|
-
var SWEPT_SESSION_WRITE_GRACE_MS = 60 * 60 * 1000;
|
|
1388
1734
|
var ACTIVE_STATUSES = new Set(["working", "blocked", "waiting"]);
|
|
1389
1735
|
// ../harmony-shared/dist/cardLinks.js
|
|
1390
1736
|
var LINK_TYPE_INVERSES = {
|
|
@@ -2612,6 +2958,9 @@ ${untrustedDataBlock(planContent.trim(), {
|
|
|
2612
2958
|
async updatePlaybook(playbookId, updates) {
|
|
2613
2959
|
return this.request("PATCH", `/playbooks/${playbookId}`, updates);
|
|
2614
2960
|
}
|
|
2961
|
+
async deletePlaybook(playbookId) {
|
|
2962
|
+
return this.request("DELETE", `/playbooks/${encodeURIComponent(playbookId)}`);
|
|
2963
|
+
}
|
|
2615
2964
|
}
|
|
2616
2965
|
var _promptModules = null;
|
|
2617
2966
|
async function loadPromptModules() {
|
|
@@ -2639,6 +2988,222 @@ function resetClient() {
|
|
|
2639
2988
|
client2 = null;
|
|
2640
2989
|
}
|
|
2641
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
|
+
|
|
2642
3207
|
// src/auto-session.ts
|
|
2643
3208
|
var CLIENT_DISPLAY_NAMES = {
|
|
2644
3209
|
"claude-code": "Claude Code",
|
|
@@ -2722,6 +3287,7 @@ async function trackActivity(cardId, options) {
|
|
|
2722
3287
|
for (const otherCardId of toEnd) {
|
|
2723
3288
|
await autoEndSession(scope, client3, otherCardId, "completed");
|
|
2724
3289
|
}
|
|
3290
|
+
let agentSessionId;
|
|
2725
3291
|
try {
|
|
2726
3292
|
const started = await client3.startAgentSession(cardId, {
|
|
2727
3293
|
agentIdentifier,
|
|
@@ -2731,6 +3297,7 @@ async function trackActivity(cardId, options) {
|
|
|
2731
3297
|
});
|
|
2732
3298
|
if (started?.session === null)
|
|
2733
3299
|
return;
|
|
3300
|
+
agentSessionId = started?.session?.id;
|
|
2734
3301
|
} catch {}
|
|
2735
3302
|
scope.sessions.set(cardId, {
|
|
2736
3303
|
cardId,
|
|
@@ -2739,8 +3306,14 @@ async function trackActivity(cardId, options) {
|
|
|
2739
3306
|
isExplicit: false,
|
|
2740
3307
|
agentIdentifier,
|
|
2741
3308
|
agentName,
|
|
3309
|
+
agentSessionId,
|
|
2742
3310
|
status: "working"
|
|
2743
3311
|
});
|
|
3312
|
+
beginHookTimeline({
|
|
3313
|
+
cardId,
|
|
3314
|
+
agentSessionId,
|
|
3315
|
+
getClient: () => client3
|
|
3316
|
+
});
|
|
2744
3317
|
}
|
|
2745
3318
|
function markExplicit(cardId, options) {
|
|
2746
3319
|
const scope = getOrCreateScope(options?.scopeId ?? DEFAULT_SCOPE);
|
|
@@ -2841,6 +3414,9 @@ function sweepTick() {
|
|
|
2841
3414
|
async function autoEndSession(scope, client3, cardId, status) {
|
|
2842
3415
|
if (!scope.sessions.delete(cardId))
|
|
2843
3416
|
return;
|
|
3417
|
+
try {
|
|
3418
|
+
await endHookTimeline(cardId);
|
|
3419
|
+
} catch {}
|
|
2844
3420
|
try {
|
|
2845
3421
|
await client3.endAgentSession(cardId, { status });
|
|
2846
3422
|
} catch {}
|
|
@@ -3447,6 +4023,51 @@ async function onboardNewUser(params) {
|
|
|
3447
4023
|
};
|
|
3448
4024
|
}
|
|
3449
4025
|
|
|
4026
|
+
// src/plan-task-link.ts
|
|
4027
|
+
function findPlanTask(tasks, taskId) {
|
|
4028
|
+
if (!taskId) {
|
|
4029
|
+
return { ok: false, reason: "No plan task id was given." };
|
|
4030
|
+
}
|
|
4031
|
+
if (!Array.isArray(tasks)) {
|
|
4032
|
+
return {
|
|
4033
|
+
ok: false,
|
|
4034
|
+
reason: "The plan returned no readable criteria list."
|
|
4035
|
+
};
|
|
4036
|
+
}
|
|
4037
|
+
for (const row of tasks) {
|
|
4038
|
+
if (!row || typeof row !== "object")
|
|
4039
|
+
continue;
|
|
4040
|
+
const candidate = row;
|
|
4041
|
+
if (candidate.id === taskId) {
|
|
4042
|
+
return { ok: true, task: candidate };
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
return {
|
|
4046
|
+
ok: false,
|
|
4047
|
+
reason: `Plan task ${taskId} is not one of this plan's criteria. ` + `Read the plan with harmony_get_plan and use an id from its \`tasks\`.`
|
|
4048
|
+
};
|
|
4049
|
+
}
|
|
4050
|
+
function linkedReport(planId, task, newCardId) {
|
|
4051
|
+
const previous = task.card_id ?? null;
|
|
4052
|
+
return {
|
|
4053
|
+
planId,
|
|
4054
|
+
taskId: task.id,
|
|
4055
|
+
linked: true,
|
|
4056
|
+
criterion: task.content ?? null,
|
|
4057
|
+
...previous && previous !== newCardId ? { replacedCardId: previous } : {}
|
|
4058
|
+
};
|
|
4059
|
+
}
|
|
4060
|
+
function unlinkedReport(planId, task, error) {
|
|
4061
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4062
|
+
return {
|
|
4063
|
+
planId,
|
|
4064
|
+
taskId: task.id,
|
|
4065
|
+
linked: false,
|
|
4066
|
+
criterion: task.content ?? null,
|
|
4067
|
+
error: `The card was created, but the plan criterion still does not point at it: ${message}. ` + `Repair it with harmony_link_plan_task — do not create the card again.`
|
|
4068
|
+
};
|
|
4069
|
+
}
|
|
4070
|
+
|
|
3450
4071
|
// src/playbook-metric-warnings.ts
|
|
3451
4072
|
function playbookMetricWarnings(agents, steps) {
|
|
3452
4073
|
if (!Array.isArray(steps))
|
|
@@ -3477,30 +4098,30 @@ async function collectPlaybookMetricWarnings(client3, workspaceId, steps) {
|
|
|
3477
4098
|
|
|
3478
4099
|
// src/skills.ts
|
|
3479
4100
|
import {
|
|
3480
|
-
existsSync as
|
|
3481
|
-
mkdirSync as
|
|
3482
|
-
readFileSync as
|
|
3483
|
-
renameSync as
|
|
3484
|
-
writeFileSync as
|
|
4101
|
+
existsSync as existsSync5,
|
|
4102
|
+
mkdirSync as mkdirSync4,
|
|
4103
|
+
readFileSync as readFileSync5,
|
|
4104
|
+
renameSync as renameSync3,
|
|
4105
|
+
writeFileSync as writeFileSync4
|
|
3485
4106
|
} from "node:fs";
|
|
3486
|
-
import { homedir as
|
|
3487
|
-
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";
|
|
3488
4109
|
init_config();
|
|
3489
4110
|
|
|
3490
4111
|
// src/hmy-config.ts
|
|
3491
|
-
import { existsSync as
|
|
3492
|
-
import { homedir as
|
|
3493
|
-
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";
|
|
3494
4115
|
var DEFAULTS = { updateCheck: true, pin: null };
|
|
3495
4116
|
function getHmyConfigPath() {
|
|
3496
|
-
return
|
|
4117
|
+
return join5(homedir3(), ".hmy", "config.yaml");
|
|
3497
4118
|
}
|
|
3498
4119
|
function loadHmyConfig() {
|
|
3499
4120
|
const path = getHmyConfigPath();
|
|
3500
|
-
if (!
|
|
4121
|
+
if (!existsSync4(path))
|
|
3501
4122
|
return { ...DEFAULTS };
|
|
3502
4123
|
try {
|
|
3503
|
-
return parseHmyConfig(
|
|
4124
|
+
return parseHmyConfig(readFileSync4(path, "utf-8"));
|
|
3504
4125
|
} catch {
|
|
3505
4126
|
return { ...DEFAULTS };
|
|
3506
4127
|
}
|
|
@@ -3632,11 +4253,11 @@ function stripSkillPreamble(content) {
|
|
|
3632
4253
|
}
|
|
3633
4254
|
function atomicWrite(filePath, content) {
|
|
3634
4255
|
const dir = dirname2(filePath);
|
|
3635
|
-
if (!
|
|
3636
|
-
|
|
4256
|
+
if (!existsSync5(dir))
|
|
4257
|
+
mkdirSync4(dir, { recursive: true });
|
|
3637
4258
|
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
3638
|
-
|
|
3639
|
-
|
|
4259
|
+
writeFileSync4(tmp, content);
|
|
4260
|
+
renameSync3(tmp, filePath);
|
|
3640
4261
|
}
|
|
3641
4262
|
function hasMetadataVersion(content) {
|
|
3642
4263
|
return parseSkillVersion(content) !== null;
|
|
@@ -3665,7 +4286,7 @@ function parseSkillVersion(content) {
|
|
|
3665
4286
|
function findSkillFiles(paths, knownNames) {
|
|
3666
4287
|
const results = [];
|
|
3667
4288
|
for (const filePath of paths) {
|
|
3668
|
-
if (!
|
|
4289
|
+
if (!existsSync5(filePath))
|
|
3669
4290
|
continue;
|
|
3670
4291
|
for (const name of knownNames) {
|
|
3671
4292
|
if (filePath.includes(`/${name}/`) || filePath.includes(`/${name}.md`)) {
|
|
@@ -3676,15 +4297,15 @@ function findSkillFiles(paths, knownNames) {
|
|
|
3676
4297
|
}
|
|
3677
4298
|
return results;
|
|
3678
4299
|
}
|
|
3679
|
-
var HMY_DIR =
|
|
3680
|
-
var HMY_VERSION_FILE =
|
|
3681
|
-
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");
|
|
3682
4303
|
var CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
3683
4304
|
function checkedRecently(now = Date.now()) {
|
|
3684
4305
|
try {
|
|
3685
|
-
if (!
|
|
4306
|
+
if (!existsSync5(LAST_CHECK_FILE))
|
|
3686
4307
|
return false;
|
|
3687
|
-
const ts = Number.parseInt(
|
|
4308
|
+
const ts = Number.parseInt(readFileSync5(LAST_CHECK_FILE, "utf-8").trim(), 10);
|
|
3688
4309
|
if (!Number.isFinite(ts))
|
|
3689
4310
|
return false;
|
|
3690
4311
|
return now - ts < CHECK_TTL_MS;
|
|
@@ -3694,9 +4315,9 @@ function checkedRecently(now = Date.now()) {
|
|
|
3694
4315
|
}
|
|
3695
4316
|
function recordCheck(now = Date.now()) {
|
|
3696
4317
|
try {
|
|
3697
|
-
if (!
|
|
3698
|
-
|
|
3699
|
-
|
|
4318
|
+
if (!existsSync5(HMY_DIR))
|
|
4319
|
+
mkdirSync4(HMY_DIR, { recursive: true });
|
|
4320
|
+
writeFileSync4(LAST_CHECK_FILE, String(now));
|
|
3700
4321
|
} catch {}
|
|
3701
4322
|
}
|
|
3702
4323
|
async function refreshSkills(opts = {}) {
|
|
@@ -3728,7 +4349,7 @@ async function refreshSkills(opts = {}) {
|
|
|
3728
4349
|
const parentDir = dirname2(samplePath);
|
|
3729
4350
|
siblingPath = `${parentDir}/${name}.md`;
|
|
3730
4351
|
}
|
|
3731
|
-
if (
|
|
4352
|
+
if (existsSync5(siblingPath)) {
|
|
3732
4353
|
skillFiles.push({ name, filePath: siblingPath });
|
|
3733
4354
|
}
|
|
3734
4355
|
}
|
|
@@ -3738,7 +4359,7 @@ async function refreshSkills(opts = {}) {
|
|
|
3738
4359
|
let updated = false;
|
|
3739
4360
|
for (const { name, filePath } of skillFiles) {
|
|
3740
4361
|
try {
|
|
3741
|
-
const currentContent =
|
|
4362
|
+
const currentContent = readFileSync5(filePath, "utf-8");
|
|
3742
4363
|
const localVersion = parseSkillVersion(currentContent);
|
|
3743
4364
|
const fetched = await client3.fetchSkill(name);
|
|
3744
4365
|
const remoteVersion = fetched.skillVersion;
|
|
@@ -3944,12 +4565,13 @@ function optionalNonNegativeNumberArg(raw, field) {
|
|
|
3944
4565
|
throw new Error(`${field} must be a non-negative number.`);
|
|
3945
4566
|
return n;
|
|
3946
4567
|
}
|
|
3947
|
-
function initMemorySession(cardId, agentIdentifier, agentName, agentSessionId) {
|
|
4568
|
+
function initMemorySession(cardId, agentIdentifier, agentName, agentSessionId, scopeId) {
|
|
3948
4569
|
memorySessions.set(cardId, {
|
|
3949
4570
|
cardId,
|
|
3950
4571
|
agentIdentifier,
|
|
3951
4572
|
agentName,
|
|
3952
4573
|
agentSessionId,
|
|
4574
|
+
scopeId,
|
|
3953
4575
|
memoryReadCount: 0,
|
|
3954
4576
|
pendingActions: [],
|
|
3955
4577
|
allActions: [],
|
|
@@ -4061,6 +4683,10 @@ var TOOLS = {
|
|
|
4061
4683
|
type: "string",
|
|
4062
4684
|
description: "Plan ID to link this card to (optional). Links the card to that plan via its plan_id."
|
|
4063
4685
|
},
|
|
4686
|
+
planTaskId: {
|
|
4687
|
+
type: "string",
|
|
4688
|
+
description: "Id of the plan CRITERION this card is created to fulfil (optional; requires `planId`). " + "Sets both directions at once: the card's plan_id, and the criterion's card_id — the " + "return leg a card outcome needs to reach the plan. Read the ids from harmony_get_plan's " + "`tasks`. A criterion that is not in the named plan refuses the whole call, so no card is " + "created on a false premise; a failure to write the return leg AFTER the card exists " + "keeps the card and reports it in `planTask` instead."
|
|
4689
|
+
},
|
|
4064
4690
|
attachments: {
|
|
4065
4691
|
type: "array",
|
|
4066
4692
|
description: "Optional reference files to attach to the new card (e.g. a screenshot from the prompt). " + "Max 5MB each; PNG/JPEG/GIF/WebP/HEIC/HEIF/PDF/DOC(X)/XLS(X)/TXT/CSV. Each file's bytes " + "come from `filePath` (absolute local path the server reads, preferred) or `base64Data` " + "(small-file fallback; requires fileName). NOTE: a pasted image only attaches if your " + "harness has written it to a local file you can pass as filePath — a model cannot re-emit " + "pasted image bytes into base64Data. Per-file failures never block card creation; they are " + "reported back in the result's `attachments` array so you can retry via harmony_upload.",
|
|
@@ -5538,6 +6164,29 @@ var TOOLS = {
|
|
|
5538
6164
|
required: ["planId"]
|
|
5539
6165
|
}
|
|
5540
6166
|
},
|
|
6167
|
+
harmony_link_plan_task: {
|
|
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.",
|
|
6169
|
+
inputSchema: {
|
|
6170
|
+
type: "object",
|
|
6171
|
+
properties: {
|
|
6172
|
+
planId: { type: "string", description: "Plan ID owning the criterion" },
|
|
6173
|
+
taskId: {
|
|
6174
|
+
type: "string",
|
|
6175
|
+
description: "Criterion id, from harmony_get_plan's `tasks`. Must belong to `planId`."
|
|
6176
|
+
},
|
|
6177
|
+
cardId: {
|
|
6178
|
+
type: "string",
|
|
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."
|
|
6180
|
+
},
|
|
6181
|
+
status: {
|
|
6182
|
+
type: "string",
|
|
6183
|
+
enum: ["pending", "in_progress", "completed"],
|
|
6184
|
+
description: "Criterion status. Set `completed` only when the card demonstrably delivered it."
|
|
6185
|
+
}
|
|
6186
|
+
},
|
|
6187
|
+
required: ["planId", "taskId"]
|
|
6188
|
+
}
|
|
6189
|
+
},
|
|
5541
6190
|
harmony_list_playbook: {
|
|
5542
6191
|
description: "List a workspace's playbooks (reusable process definitions). Returns each playbook's name, version, and state. Read-only.",
|
|
5543
6192
|
inputSchema: {
|
|
@@ -5632,6 +6281,19 @@ var TOOLS = {
|
|
|
5632
6281
|
required: ["playbookId"]
|
|
5633
6282
|
}
|
|
5634
6283
|
},
|
|
6284
|
+
harmony_delete_playbook: {
|
|
6285
|
+
description: "Permanently delete a playbook. IRREVERSIBLE and cascading: its version snapshots and run history are deleted with it, and every card currently running it is unbound — the card stays on the board and keeps its column, but loses its playbook, its pinned version and its stage pointer, so the agent daemon stops treating it as a stage card. Prefer harmony_update_playbook with state='deprecated' unless the playbook should never have existed: a deprecated playbook stays applicable to in-flight cards and is only hidden from new applies. Requires the playbook's creator or a workspace owner/admin — and an ARMED playbook (triggerType 'auto') takes an owner/admin even from its creator, because removing it stops the workspace's automation for everyone. Anyone else is refused and the playbook is left alone. Returns unboundCardCount (the exact number of cards it unbound) and unboundCards (up to 50 of them by id, short_id, title and the stage each one lost).",
|
|
6286
|
+
inputSchema: {
|
|
6287
|
+
type: "object",
|
|
6288
|
+
properties: {
|
|
6289
|
+
playbookId: {
|
|
6290
|
+
type: "string",
|
|
6291
|
+
description: "Playbook ID to delete (UUID)"
|
|
6292
|
+
}
|
|
6293
|
+
},
|
|
6294
|
+
required: ["playbookId"]
|
|
6295
|
+
}
|
|
6296
|
+
},
|
|
5635
6297
|
harmony_signup: {
|
|
5636
6298
|
description: "Create a new user account. Returns a JWT session for subsequent authenticated calls. No API key required.",
|
|
5637
6299
|
inputSchema: {
|
|
@@ -5926,28 +6588,58 @@ async function handleToolCall(name, args, deps) {
|
|
|
5926
6588
|
fileName: z.string().optional(),
|
|
5927
6589
|
contentType: z.string().optional()
|
|
5928
6590
|
})).parse(args.attachments) : [];
|
|
6591
|
+
const planId = args.planId ? z.string().uuid().parse(args.planId) : undefined;
|
|
6592
|
+
const planTaskId = args.planTaskId ? z.string().uuid().parse(args.planTaskId) : undefined;
|
|
6593
|
+
if (planTaskId && !planId) {
|
|
6594
|
+
throw new Error("planTaskId requires planId: no route resolves a plan from a criterion id alone. " + "Pass the plan the criterion belongs to — harmony_get_plan returns both.");
|
|
6595
|
+
}
|
|
6596
|
+
let criterion;
|
|
6597
|
+
if (planTaskId && planId) {
|
|
6598
|
+
const { tasks } = await client3.getPlan(planId);
|
|
6599
|
+
const found = findPlanTask(tasks, planTaskId);
|
|
6600
|
+
if (!found.ok)
|
|
6601
|
+
throw new Error(found.reason);
|
|
6602
|
+
criterion = found.task;
|
|
6603
|
+
}
|
|
5929
6604
|
const result = await client3.createCard(projectId, {
|
|
5930
6605
|
title,
|
|
5931
6606
|
columnId: args.columnId,
|
|
5932
6607
|
description: args.description,
|
|
5933
6608
|
priority: args.priority,
|
|
5934
6609
|
assigneeId: args.assigneeId,
|
|
5935
|
-
planId
|
|
6610
|
+
planId
|
|
5936
6611
|
});
|
|
6612
|
+
const newCardId = result.card?.id;
|
|
6613
|
+
let planTask;
|
|
6614
|
+
if (criterion && planId) {
|
|
6615
|
+
if (!newCardId) {
|
|
6616
|
+
planTask = unlinkedReport(planId, criterion, new Error("no card id was returned to link against"));
|
|
6617
|
+
} else {
|
|
6618
|
+
try {
|
|
6619
|
+
await client3.updatePlanTask(planId, criterion.id, {
|
|
6620
|
+
cardId: newCardId
|
|
6621
|
+
});
|
|
6622
|
+
planTask = linkedReport(planId, criterion, newCardId);
|
|
6623
|
+
} catch (err) {
|
|
6624
|
+
planTask = unlinkedReport(planId, criterion, err);
|
|
6625
|
+
}
|
|
6626
|
+
}
|
|
6627
|
+
}
|
|
6628
|
+
const planTaskField = planTask ? { planTask } : {};
|
|
5937
6629
|
if (attachments.length === 0) {
|
|
5938
|
-
return { success: true, ...result };
|
|
6630
|
+
return { success: true, ...result, ...planTaskField };
|
|
5939
6631
|
}
|
|
5940
|
-
|
|
5941
|
-
if (!cardId) {
|
|
6632
|
+
if (!newCardId) {
|
|
5942
6633
|
return {
|
|
5943
6634
|
success: true,
|
|
5944
6635
|
...result,
|
|
6636
|
+
...planTaskField,
|
|
5945
6637
|
attachmentWarning: "Card created, but attachments were skipped: no card id was returned to upload against."
|
|
5946
6638
|
};
|
|
5947
6639
|
}
|
|
5948
6640
|
const attachmentResults = await Promise.all(attachments.map(async (file) => {
|
|
5949
6641
|
try {
|
|
5950
|
-
const uploaded = await attachFileToCard(client3,
|
|
6642
|
+
const uploaded = await attachFileToCard(client3, newCardId, file);
|
|
5951
6643
|
return { ok: true, attachment: uploaded.attachment };
|
|
5952
6644
|
} catch (err) {
|
|
5953
6645
|
return {
|
|
@@ -5957,7 +6649,12 @@ async function handleToolCall(name, args, deps) {
|
|
|
5957
6649
|
};
|
|
5958
6650
|
}
|
|
5959
6651
|
}));
|
|
5960
|
-
return {
|
|
6652
|
+
return {
|
|
6653
|
+
success: true,
|
|
6654
|
+
...result,
|
|
6655
|
+
...planTaskField,
|
|
6656
|
+
attachments: attachmentResults
|
|
6657
|
+
};
|
|
5961
6658
|
}
|
|
5962
6659
|
case "harmony_update_card": {
|
|
5963
6660
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
@@ -6441,15 +7138,24 @@ ${list}
|
|
|
6441
7138
|
const supersedesId = args.supersedesId !== undefined ? z.string().uuid().parse(args.supersedesId) : undefined;
|
|
6442
7139
|
const confirmsId = args.confirmsId !== undefined ? z.string().uuid().parse(args.confirmsId) : undefined;
|
|
6443
7140
|
const replyToId = args.replyToId !== undefined ? z.string().uuid().parse(args.replyToId) : undefined;
|
|
6444
|
-
const
|
|
7141
|
+
const sessionChoice = chooseCommentSession({
|
|
7142
|
+
cardId,
|
|
7143
|
+
tracked: getMemorySession(cardId),
|
|
7144
|
+
callerScopeId: deps.getScopeId?.(),
|
|
7145
|
+
declared: readDeclaredRunSession()
|
|
7146
|
+
});
|
|
6445
7147
|
const result = await client3.addComment(cardId, body, {
|
|
6446
7148
|
commentType,
|
|
6447
7149
|
supersedesId,
|
|
6448
7150
|
confirmsId,
|
|
6449
7151
|
replyToId,
|
|
6450
|
-
agentSessionId
|
|
7152
|
+
agentSessionId: sessionChoice.kind === "session" ? sessionChoice.agentSessionId : undefined
|
|
6451
7153
|
});
|
|
6452
|
-
return {
|
|
7154
|
+
return {
|
|
7155
|
+
success: true,
|
|
7156
|
+
...result,
|
|
7157
|
+
sessionAttribution: sessionChoice.kind === "session" ? sessionChoice.source : "none"
|
|
7158
|
+
};
|
|
6453
7159
|
}
|
|
6454
7160
|
case "harmony_get_comments": {
|
|
6455
7161
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
@@ -6673,7 +7379,12 @@ ${options}
|
|
|
6673
7379
|
scopeId: deps.getScopeId?.()
|
|
6674
7380
|
});
|
|
6675
7381
|
const agentSessionId = result.session?.id;
|
|
6676
|
-
initMemorySession(cardId, agentIdentifier, agentName, agentSessionId);
|
|
7382
|
+
initMemorySession(cardId, agentIdentifier, agentName, agentSessionId, deps.getScopeId?.());
|
|
7383
|
+
beginHookTimeline({
|
|
7384
|
+
cardId,
|
|
7385
|
+
agentSessionId,
|
|
7386
|
+
getClient: () => client3
|
|
7387
|
+
});
|
|
6677
7388
|
return {
|
|
6678
7389
|
success: true,
|
|
6679
7390
|
assignedTo,
|
|
@@ -6735,6 +7446,7 @@ ${options}
|
|
|
6735
7446
|
const endProgressPercent = optionalPercentArg(args.progressPercent, "progressPercent");
|
|
6736
7447
|
await flushMemoryActions(client3, cardId);
|
|
6737
7448
|
cleanupMemorySession(cardId);
|
|
7449
|
+
await endHookTimeline(cardId);
|
|
6738
7450
|
let result = {
|
|
6739
7451
|
session: null
|
|
6740
7452
|
};
|
|
@@ -7344,7 +8056,7 @@ ${options}
|
|
|
7344
8056
|
} else if (args.cardId) {
|
|
7345
8057
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
7346
8058
|
result = await client3.getPlanByCardId(cardId);
|
|
7347
|
-
if (!result) {
|
|
8059
|
+
if (!result || result.plan == null && !result.foreign_criteria?.length) {
|
|
7348
8060
|
return {
|
|
7349
8061
|
success: true,
|
|
7350
8062
|
plan: null,
|
|
@@ -7352,13 +8064,23 @@ ${options}
|
|
|
7352
8064
|
message: "No plan linked to this card"
|
|
7353
8065
|
};
|
|
7354
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
|
+
}
|
|
7355
8076
|
} else {
|
|
7356
8077
|
throw new Error("Either planId or cardId must be provided");
|
|
7357
8078
|
}
|
|
7358
8079
|
return {
|
|
7359
8080
|
success: true,
|
|
7360
8081
|
plan: result.plan,
|
|
7361
|
-
tasks: result.tasks
|
|
8082
|
+
tasks: result.tasks,
|
|
8083
|
+
...result.foreign_criteria?.length ? { divergentCriteria: result.foreign_criteria } : {}
|
|
7362
8084
|
};
|
|
7363
8085
|
}
|
|
7364
8086
|
case "harmony_update_plan": {
|
|
@@ -7374,6 +8096,34 @@ ${options}
|
|
|
7374
8096
|
const result = await client3.updatePlan(planId, updates);
|
|
7375
8097
|
return { success: true, plan: result.plan };
|
|
7376
8098
|
}
|
|
8099
|
+
case "harmony_link_plan_task": {
|
|
8100
|
+
const planId = z.string().uuid().parse(args.planId);
|
|
8101
|
+
const taskId = z.string().uuid().parse(args.taskId);
|
|
8102
|
+
const cardId = args.cardId ? z.string().uuid().parse(args.cardId) : undefined;
|
|
8103
|
+
const status = args.status ? z.enum(["pending", "in_progress", "completed"]).parse(args.status) : undefined;
|
|
8104
|
+
if (!cardId && !status) {
|
|
8105
|
+
throw new Error("Nothing to do: pass cardId, status, or both.");
|
|
8106
|
+
}
|
|
8107
|
+
const { tasks } = await client3.getPlan(planId);
|
|
8108
|
+
const found = findPlanTask(tasks, taskId);
|
|
8109
|
+
if (!found.ok)
|
|
8110
|
+
throw new Error(found.reason);
|
|
8111
|
+
const result = await client3.updatePlanTask(planId, taskId, {
|
|
8112
|
+
cardId,
|
|
8113
|
+
status
|
|
8114
|
+
});
|
|
8115
|
+
return {
|
|
8116
|
+
success: true,
|
|
8117
|
+
planTask: {
|
|
8118
|
+
planId,
|
|
8119
|
+
taskId,
|
|
8120
|
+
criterion: found.task.content ?? null,
|
|
8121
|
+
...cardId ? linkedReport(planId, found.task, cardId) : { linked: found.task.card_id != null },
|
|
8122
|
+
...cardId && result.cardPlanAdopted ? { cardPlanAdopted: true } : {},
|
|
8123
|
+
...status ? { status } : {}
|
|
8124
|
+
}
|
|
8125
|
+
};
|
|
8126
|
+
}
|
|
7377
8127
|
case "harmony_advance_plan": {
|
|
7378
8128
|
const planId = z.string().uuid().parse(args.planId);
|
|
7379
8129
|
const summary = args.summary;
|
|
@@ -7461,6 +8211,16 @@ ${options}
|
|
|
7461
8211
|
...warnings.length > 0 ? { warnings } : {}
|
|
7462
8212
|
};
|
|
7463
8213
|
}
|
|
8214
|
+
case "harmony_delete_playbook": {
|
|
8215
|
+
const playbookId = z.string().uuid().parse(args.playbookId);
|
|
8216
|
+
const result = await client3.deletePlaybook(playbookId);
|
|
8217
|
+
return {
|
|
8218
|
+
success: true,
|
|
8219
|
+
playbook: result.playbook,
|
|
8220
|
+
unboundCardCount: result.unboundCardCount,
|
|
8221
|
+
unboundCards: result.unboundCards
|
|
8222
|
+
};
|
|
8223
|
+
}
|
|
7464
8224
|
case "harmony_save_card_as_playbook":
|
|
7465
8225
|
return deprecatedRemovedToolResult("harmony_save_card_as_playbook");
|
|
7466
8226
|
case "harmony_signup": {
|
|
@@ -7594,6 +8354,9 @@ class HarmonyMCPServer {
|
|
|
7594
8354
|
try {
|
|
7595
8355
|
await shutdownAllSessions();
|
|
7596
8356
|
} catch {}
|
|
8357
|
+
try {
|
|
8358
|
+
await stopAllRunEventForwarders();
|
|
8359
|
+
} catch {}
|
|
7597
8360
|
destroyAutoSession();
|
|
7598
8361
|
process.exit(exitCode);
|
|
7599
8362
|
};
|