@gethmy/mcp 2.20.0 → 2.21.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/dist/cli.js +72 -15
- package/dist/index.js +53 -12
- package/dist/lib/api-client.js +8 -0
- package/package.json +2 -2
- package/src/api-client.ts +69 -5
- package/src/auto-session.ts +25 -6
- package/src/remote.ts +616 -266
- package/src/server.ts +106 -11
- package/src/tui/setup.ts +19 -3
package/dist/cli.js
CHANGED
|
@@ -1386,6 +1386,14 @@ import {
|
|
|
1386
1386
|
ReadResourceRequestSchema
|
|
1387
1387
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
1388
1388
|
import { z } from "zod";
|
|
1389
|
+
// ../harmony-shared/dist/agentStaleness.js
|
|
1390
|
+
var AGENT_HEARTBEAT_LIVENESS_MS = 5 * 60 * 1000;
|
|
1391
|
+
var AGENT_MILESTONE_LIVENESS_MS = 30 * 60 * 1000;
|
|
1392
|
+
var AGENT_SWEEP_DAEMON_MS = 30 * 60 * 1000;
|
|
1393
|
+
var AGENT_SWEEP_INTERACTIVE_MS = 2 * 60 * 60 * 1000;
|
|
1394
|
+
var AGENT_SWEEP_PAUSED_MS = 4 * 60 * 60 * 1000;
|
|
1395
|
+
var SWEPT_SESSION_WRITE_GRACE_MS = 60 * 60 * 1000;
|
|
1396
|
+
var ACTIVE_STATUSES = new Set(["working", "blocked", "waiting"]);
|
|
1389
1397
|
// ../harmony-shared/dist/cardLinks.js
|
|
1390
1398
|
var LINK_TYPE_INVERSES = {
|
|
1391
1399
|
relates_to: "relates_to",
|
|
@@ -2556,11 +2564,14 @@ async function trackActivity(cardId, options) {
|
|
|
2556
2564
|
await autoEndSession(scope, client3, otherCardId, "completed");
|
|
2557
2565
|
}
|
|
2558
2566
|
try {
|
|
2559
|
-
await client3.startAgentSession(cardId, {
|
|
2567
|
+
const started = await client3.startAgentSession(cardId, {
|
|
2560
2568
|
agentIdentifier,
|
|
2561
2569
|
agentName,
|
|
2562
|
-
status: "working"
|
|
2570
|
+
status: "working",
|
|
2571
|
+
implicitCreate: true
|
|
2563
2572
|
});
|
|
2573
|
+
if (started?.session === null)
|
|
2574
|
+
return;
|
|
2564
2575
|
} catch {}
|
|
2565
2576
|
scope.sessions.set(cardId, {
|
|
2566
2577
|
cardId,
|
|
@@ -3798,7 +3809,8 @@ async function flushMemoryActions(client3, cardId) {
|
|
|
3798
3809
|
await client3.updateAgentProgress(cardId, {
|
|
3799
3810
|
agentIdentifier: session.agentIdentifier,
|
|
3800
3811
|
agentName: session.agentName,
|
|
3801
|
-
recentActions: session.allActions
|
|
3812
|
+
recentActions: session.allActions,
|
|
3813
|
+
implicitCreate: true
|
|
3802
3814
|
});
|
|
3803
3815
|
session.dirty = false;
|
|
3804
3816
|
} catch (err) {
|
|
@@ -4190,7 +4202,7 @@ var TOOLS = {
|
|
|
4190
4202
|
}
|
|
4191
4203
|
},
|
|
4192
4204
|
harmony_upload: {
|
|
4193
|
-
description: 'Upload a file in one call. `target: "card_attachment"` attaches a file to a card ' + "(max 5MB; PNG/JPEG/GIF/WebP/HEIC/HEIF/PDF/DOC(X)/XLS(X)/TXT/CSV) — requires cardId. " + '`target: "artifact"` hosts a self-contained HTML doc (text/html, max 2MB) linked to ' + "exactly one of cardId/planId/workspaceId, rendered in-app in a sandboxed iframe. Provide " + "the bytes as `filePath` (local, direct-to-storage) or `base64Data` (small-file fallback). " + "Returns the attachment/artifact + a
|
|
4205
|
+
description: 'Upload a file in one call. `target: "card_attachment"` attaches a file to a card ' + "(max 5MB; PNG/JPEG/GIF/WebP/HEIC/HEIF/PDF/DOC(X)/XLS(X)/TXT/CSV) — requires cardId. " + '`target: "artifact"` hosts a self-contained HTML doc (text/html, max 2MB) linked to ' + "exactly one of cardId/planId/workspaceId, rendered in-app in a sandboxed iframe. Provide " + "the bytes as `filePath` (local, direct-to-storage) or `base64Data` (small-file fallback). " + "Returns the attachment/artifact. An artifact comes back with TWO URLs, and the difference " + "matters: `app_url` is the durable in-app permalink — this is the one to hand a person, it " + "never expires and requires them to log in as a workspace member — while `signed_url` only " + "renders the document in-app and dies within the hour, so never pass it on. To share with " + "someone OUTSIDE the workspace, mint a public link with harmony_share_artifact. Large files " + "on the hosted MCP server: use harmony_request_upload_url + harmony_finalize_upload instead.",
|
|
4194
4206
|
inputSchema: {
|
|
4195
4207
|
type: "object",
|
|
4196
4208
|
properties: {
|
|
@@ -4276,7 +4288,7 @@ var TOOLS = {
|
|
|
4276
4288
|
}
|
|
4277
4289
|
},
|
|
4278
4290
|
harmony_finalize_upload: {
|
|
4279
|
-
description: "Step 2 of the upload handshake. After PUTting the bytes to the signed uploadUrl, call this " + "with the storagePath to validate and register. The server re-downloads the object and " + "enforces size + content-type (magic-byte sniff, never the declared type) + an optional " + 'sha256 integrity check, deleting and failing on any mismatch. `target: "card_attachment"` ' + '(cardId, storagePath, fileName) or `target: "artifact"` (storagePath + the same one of ' + "cardId/planId/workspaceId used for the upload URL). Returns the attachment/artifact + a signed URL
|
|
4291
|
+
description: "Step 2 of the upload handshake. After PUTting the bytes to the signed uploadUrl, call this " + "with the storagePath to validate and register. The server re-downloads the object and " + "enforces size + content-type (magic-byte sniff, never the declared type) + an optional " + 'sha256 integrity check, deleting and failing on any mismatch. `target: "card_attachment"` ' + '(cardId, storagePath, fileName) or `target: "artifact"` (storagePath + the same one of ' + "cardId/planId/workspaceId used for the upload URL). Returns the attachment/artifact + a signed " + "URL; an artifact also carries `app_url`, the durable in-app permalink — hand that one to a " + "person, never the short-lived `signed_url`.",
|
|
4280
4292
|
inputSchema: {
|
|
4281
4293
|
type: "object",
|
|
4282
4294
|
properties: {
|
|
@@ -4330,7 +4342,7 @@ var TOOLS = {
|
|
|
4330
4342
|
}
|
|
4331
4343
|
},
|
|
4332
4344
|
harmony_share_artifact: {
|
|
4333
|
-
description: "Create a public, unauthenticated share link for a hosted artifact. Anyone with the link can view the rendered HTML without a Harmony account. Returns the share token and the full public URL.",
|
|
4345
|
+
description: "Create a public, unauthenticated share link for a hosted artifact. Anyone with the link can view the rendered HTML without a Harmony account. Returns the share token and the full public URL. Only needed for a recipient OUTSIDE the workspace — for a teammate, hand over the `app_url` the upload already returned instead of exposing the document publicly.",
|
|
4334
4346
|
inputSchema: {
|
|
4335
4347
|
type: "object",
|
|
4336
4348
|
properties: {
|
|
@@ -4651,7 +4663,7 @@ var TOOLS = {
|
|
|
4651
4663
|
}
|
|
4652
4664
|
},
|
|
4653
4665
|
harmony_update_agent_progress: {
|
|
4654
|
-
description: "Update progress on an active agent session. Use to report progress percentage, current task, blockers, or status changes.",
|
|
4666
|
+
description: "Update progress on an active agent session. Use to report progress percentage, current task, blockers, or status changes. Check the reply: `session: null` with `stopped: true` means a human stopped this card's run, so reporting progress will no longer open a session here (card #770) — stop work if you were mid-run, and read `recovery` for the one legitimate way back. Otherwise a session is always returned: this call opens one if none is live.",
|
|
4655
4667
|
inputSchema: {
|
|
4656
4668
|
type: "object",
|
|
4657
4669
|
properties: {
|
|
@@ -4736,7 +4748,7 @@ var TOOLS = {
|
|
|
4736
4748
|
}
|
|
4737
4749
|
},
|
|
4738
4750
|
harmony_get_pending_messages: {
|
|
4739
|
-
description: "Drain queued steering messages a teammate sent to your live agent session (
|
|
4751
|
+
description: "Drain queued steering messages a teammate sent to your live agent session, and check whether that session is still live (cards #473, #770). Call at your progress checkpoints with the session id from harmony_start_agent_session and the highest seq you've already consumed; returns user messages with seq > sinceSeq, oldest first. Fold them into your next step and advance sinceSeq to the largest returned seq so each is handled exactly once. Two liveness flags come back and mean different things: `stopped` (a human pressed Stop) is TERMINAL — stop work at once, make no further edits, commits, pushes, or progress writes, don't move the card, and report what you had done and where any uncommitted work lives. `sessionStale` only means your session id went stale (usually the 30-minute inactivity cron) — nobody stopped you, so do NOT abandon the work: open a fresh session with harmony_start_agent_session and carry on with its new id. (Distinct from harmony_move_card's `sessionEnded`, which means the move deliberately closed your session.)",
|
|
4740
4752
|
inputSchema: {
|
|
4741
4753
|
type: "object",
|
|
4742
4754
|
properties: {
|
|
@@ -5583,7 +5595,7 @@ function registerHandlers(server, deps) {
|
|
|
5583
5595
|
trackActivity(cardIdArg, {
|
|
5584
5596
|
autoStart: isAutoStartTrigger,
|
|
5585
5597
|
client: deps.getClient(),
|
|
5586
|
-
clientInfo: cv ? { name: cv.name, version: cv.version } : undefined,
|
|
5598
|
+
clientInfo: cv ? { name: cv.name, version: cv.version } : deps.getClientInfo?.() ?? undefined,
|
|
5587
5599
|
scopeId: deps.getScopeId?.()
|
|
5588
5600
|
}).catch(() => {});
|
|
5589
5601
|
}
|
|
@@ -5598,7 +5610,7 @@ function registerHandlers(server, deps) {
|
|
|
5598
5610
|
trackActivity(resolvedCardId, {
|
|
5599
5611
|
autoStart: true,
|
|
5600
5612
|
client: deps.getClient(),
|
|
5601
|
-
clientInfo: cv ? { name: cv.name, version: cv.version } : undefined,
|
|
5613
|
+
clientInfo: cv ? { name: cv.name, version: cv.version } : deps.getClientInfo?.() ?? undefined,
|
|
5602
5614
|
scopeId: deps.getScopeId?.()
|
|
5603
5615
|
}).catch(() => {});
|
|
5604
5616
|
}
|
|
@@ -6454,8 +6466,12 @@ ${options}
|
|
|
6454
6466
|
blockers: args.blockers,
|
|
6455
6467
|
estimatedMinutesRemaining: optionalNonNegativeNumberArg(args.estimatedMinutesRemaining, "estimatedMinutesRemaining"),
|
|
6456
6468
|
...mergedRecentActions && { recentActions: mergedRecentActions },
|
|
6457
|
-
...runActivity.length > 0 && { runActivity }
|
|
6469
|
+
...runActivity.length > 0 && { runActivity },
|
|
6470
|
+
implicitCreate: true
|
|
6458
6471
|
});
|
|
6472
|
+
if (result.session === null) {
|
|
6473
|
+
untrack(cardId, deps.getScopeId?.());
|
|
6474
|
+
}
|
|
6459
6475
|
return { success: true, midSessionLearnings: 0, ...result };
|
|
6460
6476
|
}
|
|
6461
6477
|
case "harmony_end_agent_session": {
|
|
@@ -6465,7 +6481,9 @@ ${options}
|
|
|
6465
6481
|
const endProgressPercent = optionalPercentArg(args.progressPercent, "progressPercent");
|
|
6466
6482
|
await flushMemoryActions(client3, cardId);
|
|
6467
6483
|
cleanupMemorySession(cardId);
|
|
6468
|
-
let result = {
|
|
6484
|
+
let result = {
|
|
6485
|
+
session: null
|
|
6486
|
+
};
|
|
6469
6487
|
let sessionEndError = null;
|
|
6470
6488
|
try {
|
|
6471
6489
|
result = await client3.endAgentSession(cardId, {
|
|
@@ -6515,6 +6533,29 @@ ${options}
|
|
|
6515
6533
|
const sessionId = z.string().min(1).parse(args.sessionId);
|
|
6516
6534
|
const sinceSeq = args.sinceSeq !== undefined ? z.number().int().min(0).parse(args.sinceSeq) : 0;
|
|
6517
6535
|
const result = await client3.getPendingMessages(cardId, sessionId, sinceSeq);
|
|
6536
|
+
const stoppedByHuman = result.session?.stoppedByHuman === true;
|
|
6537
|
+
const ended = result.session?.ended === true;
|
|
6538
|
+
if (ended || stoppedByHuman) {
|
|
6539
|
+
untrack(cardId, deps.getScopeId?.());
|
|
6540
|
+
}
|
|
6541
|
+
if (stoppedByHuman) {
|
|
6542
|
+
return {
|
|
6543
|
+
success: true,
|
|
6544
|
+
...result,
|
|
6545
|
+
stopped: true,
|
|
6546
|
+
stopReason: "human_stopped",
|
|
6547
|
+
instruction: "A human stopped this run — stop work now. Make no further edits, commits, pushes, or progress writes, and do not move the card. Commit any uncommitted work in place so it isn't lost, then report to the user what you completed, what is unfinished, and the branch holding it."
|
|
6548
|
+
};
|
|
6549
|
+
}
|
|
6550
|
+
if (ended) {
|
|
6551
|
+
return {
|
|
6552
|
+
success: true,
|
|
6553
|
+
...result,
|
|
6554
|
+
sessionStale: true,
|
|
6555
|
+
staleReason: result.session?.status,
|
|
6556
|
+
instruction: 'This session id is no longer live (it was closed as "' + (result.session?.status ?? "missing") + '") — but nobody stopped you. Do NOT abandon the work. Open a fresh session with harmony_start_agent_session, use its new id for later polls, and carry on.'
|
|
6557
|
+
};
|
|
6558
|
+
}
|
|
6518
6559
|
return { success: true, ...result };
|
|
6519
6560
|
}
|
|
6520
6561
|
case "harmony_generate_prompt": {
|
|
@@ -8546,14 +8587,29 @@ ${summary}`);
|
|
|
8546
8587
|
|
|
8547
8588
|
This project uses Harmony for task management. When working on tasks:
|
|
8548
8589
|
|
|
8590
|
+
## Agent identity — always identify as yourself
|
|
8591
|
+
|
|
8592
|
+
Every \`harmony_start_agent_session\` call passes \`agentIdentifier\` + \`agentName\`. **Use your own
|
|
8593
|
+
identity, never a hardcoded one from this file.** AGENTS.md is a cross-runtime convention file, so
|
|
8594
|
+
more than one kind of agent will read it; the board shows agents as teammates, and a session
|
|
8595
|
+
attributed to the wrong runtime misattributes the work in front of the whole team.
|
|
8596
|
+
|
|
8597
|
+
- \`agentIdentifier\` — a stable kebab-case id for the runtime you actually are
|
|
8598
|
+
- \`agentName\` — its human-readable name
|
|
8599
|
+
|
|
8600
|
+
Known values: \`claude-code\` / "Claude Code", \`codex\` / "OpenAI Codex", \`cursor\` / "Cursor",
|
|
8601
|
+
\`claude-desktop\` / "Claude Desktop". If you are a runtime not listed here, use your own name rather
|
|
8602
|
+
than borrowing the closest entry.
|
|
8603
|
+
|
|
8549
8604
|
## Starting Work on a Card
|
|
8550
8605
|
|
|
8551
8606
|
When given a card reference (e.g., #42 or a card name), follow this workflow:
|
|
8552
8607
|
|
|
8553
|
-
1. Use \`
|
|
8608
|
+
1. Use \`harmony_get_card\` or \`harmony_search_cards\` to find the card
|
|
8554
8609
|
2. Move the card to "In Progress" using \`harmony_move_card\`
|
|
8555
8610
|
3. Add the "agent" label using \`harmony_add_label_to_card\`
|
|
8556
|
-
4. Start a session with \`harmony_start_agent_session
|
|
8611
|
+
4. Start a session with \`harmony_start_agent_session\`, passing **your own** \`agentIdentifier\` +
|
|
8612
|
+
\`agentName\` (see "Agent identity" above)
|
|
8557
8613
|
5. Show the card details to the user
|
|
8558
8614
|
6. Use \`harmony_generate_prompt\` to get guidance, then implement the solution
|
|
8559
8615
|
7. Update progress periodically with \`harmony_update_agent_progress\`
|
|
@@ -8564,7 +8620,8 @@ When given a card reference (e.g., #42 or a card name), follow this workflow:
|
|
|
8564
8620
|
Before implementing a plan or feature, check if it maps to an existing Harmony card:
|
|
8565
8621
|
|
|
8566
8622
|
1. Use \`harmony_search_cards\` with keywords from the task description
|
|
8567
|
-
2. If a match is found, call \`harmony_start_agent_session\`
|
|
8623
|
+
2. If a match is found, call \`harmony_start_agent_session\` with **your own** \`agentIdentifier\` +
|
|
8624
|
+
\`agentName\` (see "Agent identity" above), plus \`moveToColumn: "In Progress"\`, \`addLabels: ["agent"]\`
|
|
8568
8625
|
3. Update progress with \`harmony_update_agent_progress\` at milestones
|
|
8569
8626
|
4. When done, call \`harmony_end_agent_session\` with status: "completed", moveToColumn: "Review"
|
|
8570
8627
|
|
package/dist/index.js
CHANGED
|
@@ -1381,6 +1381,14 @@ import {
|
|
|
1381
1381
|
ReadResourceRequestSchema
|
|
1382
1382
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
1383
1383
|
import { z } from "zod";
|
|
1384
|
+
// ../harmony-shared/dist/agentStaleness.js
|
|
1385
|
+
var AGENT_HEARTBEAT_LIVENESS_MS = 5 * 60 * 1000;
|
|
1386
|
+
var AGENT_MILESTONE_LIVENESS_MS = 30 * 60 * 1000;
|
|
1387
|
+
var AGENT_SWEEP_DAEMON_MS = 30 * 60 * 1000;
|
|
1388
|
+
var AGENT_SWEEP_INTERACTIVE_MS = 2 * 60 * 60 * 1000;
|
|
1389
|
+
var AGENT_SWEEP_PAUSED_MS = 4 * 60 * 60 * 1000;
|
|
1390
|
+
var SWEPT_SESSION_WRITE_GRACE_MS = 60 * 60 * 1000;
|
|
1391
|
+
var ACTIVE_STATUSES = new Set(["working", "blocked", "waiting"]);
|
|
1384
1392
|
// ../harmony-shared/dist/cardLinks.js
|
|
1385
1393
|
var LINK_TYPE_INVERSES = {
|
|
1386
1394
|
relates_to: "relates_to",
|
|
@@ -2551,11 +2559,14 @@ async function trackActivity(cardId, options) {
|
|
|
2551
2559
|
await autoEndSession(scope, client3, otherCardId, "completed");
|
|
2552
2560
|
}
|
|
2553
2561
|
try {
|
|
2554
|
-
await client3.startAgentSession(cardId, {
|
|
2562
|
+
const started = await client3.startAgentSession(cardId, {
|
|
2555
2563
|
agentIdentifier,
|
|
2556
2564
|
agentName,
|
|
2557
|
-
status: "working"
|
|
2565
|
+
status: "working",
|
|
2566
|
+
implicitCreate: true
|
|
2558
2567
|
});
|
|
2568
|
+
if (started?.session === null)
|
|
2569
|
+
return;
|
|
2559
2570
|
} catch {}
|
|
2560
2571
|
scope.sessions.set(cardId, {
|
|
2561
2572
|
cardId,
|
|
@@ -3793,7 +3804,8 @@ async function flushMemoryActions(client3, cardId) {
|
|
|
3793
3804
|
await client3.updateAgentProgress(cardId, {
|
|
3794
3805
|
agentIdentifier: session.agentIdentifier,
|
|
3795
3806
|
agentName: session.agentName,
|
|
3796
|
-
recentActions: session.allActions
|
|
3807
|
+
recentActions: session.allActions,
|
|
3808
|
+
implicitCreate: true
|
|
3797
3809
|
});
|
|
3798
3810
|
session.dirty = false;
|
|
3799
3811
|
} catch (err) {
|
|
@@ -4185,7 +4197,7 @@ var TOOLS = {
|
|
|
4185
4197
|
}
|
|
4186
4198
|
},
|
|
4187
4199
|
harmony_upload: {
|
|
4188
|
-
description: 'Upload a file in one call. `target: "card_attachment"` attaches a file to a card ' + "(max 5MB; PNG/JPEG/GIF/WebP/HEIC/HEIF/PDF/DOC(X)/XLS(X)/TXT/CSV) — requires cardId. " + '`target: "artifact"` hosts a self-contained HTML doc (text/html, max 2MB) linked to ' + "exactly one of cardId/planId/workspaceId, rendered in-app in a sandboxed iframe. Provide " + "the bytes as `filePath` (local, direct-to-storage) or `base64Data` (small-file fallback). " + "Returns the attachment/artifact + a
|
|
4200
|
+
description: 'Upload a file in one call. `target: "card_attachment"` attaches a file to a card ' + "(max 5MB; PNG/JPEG/GIF/WebP/HEIC/HEIF/PDF/DOC(X)/XLS(X)/TXT/CSV) — requires cardId. " + '`target: "artifact"` hosts a self-contained HTML doc (text/html, max 2MB) linked to ' + "exactly one of cardId/planId/workspaceId, rendered in-app in a sandboxed iframe. Provide " + "the bytes as `filePath` (local, direct-to-storage) or `base64Data` (small-file fallback). " + "Returns the attachment/artifact. An artifact comes back with TWO URLs, and the difference " + "matters: `app_url` is the durable in-app permalink — this is the one to hand a person, it " + "never expires and requires them to log in as a workspace member — while `signed_url` only " + "renders the document in-app and dies within the hour, so never pass it on. To share with " + "someone OUTSIDE the workspace, mint a public link with harmony_share_artifact. Large files " + "on the hosted MCP server: use harmony_request_upload_url + harmony_finalize_upload instead.",
|
|
4189
4201
|
inputSchema: {
|
|
4190
4202
|
type: "object",
|
|
4191
4203
|
properties: {
|
|
@@ -4271,7 +4283,7 @@ var TOOLS = {
|
|
|
4271
4283
|
}
|
|
4272
4284
|
},
|
|
4273
4285
|
harmony_finalize_upload: {
|
|
4274
|
-
description: "Step 2 of the upload handshake. After PUTting the bytes to the signed uploadUrl, call this " + "with the storagePath to validate and register. The server re-downloads the object and " + "enforces size + content-type (magic-byte sniff, never the declared type) + an optional " + 'sha256 integrity check, deleting and failing on any mismatch. `target: "card_attachment"` ' + '(cardId, storagePath, fileName) or `target: "artifact"` (storagePath + the same one of ' + "cardId/planId/workspaceId used for the upload URL). Returns the attachment/artifact + a signed URL
|
|
4286
|
+
description: "Step 2 of the upload handshake. After PUTting the bytes to the signed uploadUrl, call this " + "with the storagePath to validate and register. The server re-downloads the object and " + "enforces size + content-type (magic-byte sniff, never the declared type) + an optional " + 'sha256 integrity check, deleting and failing on any mismatch. `target: "card_attachment"` ' + '(cardId, storagePath, fileName) or `target: "artifact"` (storagePath + the same one of ' + "cardId/planId/workspaceId used for the upload URL). Returns the attachment/artifact + a signed " + "URL; an artifact also carries `app_url`, the durable in-app permalink — hand that one to a " + "person, never the short-lived `signed_url`.",
|
|
4275
4287
|
inputSchema: {
|
|
4276
4288
|
type: "object",
|
|
4277
4289
|
properties: {
|
|
@@ -4325,7 +4337,7 @@ var TOOLS = {
|
|
|
4325
4337
|
}
|
|
4326
4338
|
},
|
|
4327
4339
|
harmony_share_artifact: {
|
|
4328
|
-
description: "Create a public, unauthenticated share link for a hosted artifact. Anyone with the link can view the rendered HTML without a Harmony account. Returns the share token and the full public URL.",
|
|
4340
|
+
description: "Create a public, unauthenticated share link for a hosted artifact. Anyone with the link can view the rendered HTML without a Harmony account. Returns the share token and the full public URL. Only needed for a recipient OUTSIDE the workspace — for a teammate, hand over the `app_url` the upload already returned instead of exposing the document publicly.",
|
|
4329
4341
|
inputSchema: {
|
|
4330
4342
|
type: "object",
|
|
4331
4343
|
properties: {
|
|
@@ -4646,7 +4658,7 @@ var TOOLS = {
|
|
|
4646
4658
|
}
|
|
4647
4659
|
},
|
|
4648
4660
|
harmony_update_agent_progress: {
|
|
4649
|
-
description: "Update progress on an active agent session. Use to report progress percentage, current task, blockers, or status changes.",
|
|
4661
|
+
description: "Update progress on an active agent session. Use to report progress percentage, current task, blockers, or status changes. Check the reply: `session: null` with `stopped: true` means a human stopped this card's run, so reporting progress will no longer open a session here (card #770) — stop work if you were mid-run, and read `recovery` for the one legitimate way back. Otherwise a session is always returned: this call opens one if none is live.",
|
|
4650
4662
|
inputSchema: {
|
|
4651
4663
|
type: "object",
|
|
4652
4664
|
properties: {
|
|
@@ -4731,7 +4743,7 @@ var TOOLS = {
|
|
|
4731
4743
|
}
|
|
4732
4744
|
},
|
|
4733
4745
|
harmony_get_pending_messages: {
|
|
4734
|
-
description: "Drain queued steering messages a teammate sent to your live agent session (
|
|
4746
|
+
description: "Drain queued steering messages a teammate sent to your live agent session, and check whether that session is still live (cards #473, #770). Call at your progress checkpoints with the session id from harmony_start_agent_session and the highest seq you've already consumed; returns user messages with seq > sinceSeq, oldest first. Fold them into your next step and advance sinceSeq to the largest returned seq so each is handled exactly once. Two liveness flags come back and mean different things: `stopped` (a human pressed Stop) is TERMINAL — stop work at once, make no further edits, commits, pushes, or progress writes, don't move the card, and report what you had done and where any uncommitted work lives. `sessionStale` only means your session id went stale (usually the 30-minute inactivity cron) — nobody stopped you, so do NOT abandon the work: open a fresh session with harmony_start_agent_session and carry on with its new id. (Distinct from harmony_move_card's `sessionEnded`, which means the move deliberately closed your session.)",
|
|
4735
4747
|
inputSchema: {
|
|
4736
4748
|
type: "object",
|
|
4737
4749
|
properties: {
|
|
@@ -5578,7 +5590,7 @@ function registerHandlers(server, deps) {
|
|
|
5578
5590
|
trackActivity(cardIdArg, {
|
|
5579
5591
|
autoStart: isAutoStartTrigger,
|
|
5580
5592
|
client: deps.getClient(),
|
|
5581
|
-
clientInfo: cv ? { name: cv.name, version: cv.version } : undefined,
|
|
5593
|
+
clientInfo: cv ? { name: cv.name, version: cv.version } : deps.getClientInfo?.() ?? undefined,
|
|
5582
5594
|
scopeId: deps.getScopeId?.()
|
|
5583
5595
|
}).catch(() => {});
|
|
5584
5596
|
}
|
|
@@ -5593,7 +5605,7 @@ function registerHandlers(server, deps) {
|
|
|
5593
5605
|
trackActivity(resolvedCardId, {
|
|
5594
5606
|
autoStart: true,
|
|
5595
5607
|
client: deps.getClient(),
|
|
5596
|
-
clientInfo: cv ? { name: cv.name, version: cv.version } : undefined,
|
|
5608
|
+
clientInfo: cv ? { name: cv.name, version: cv.version } : deps.getClientInfo?.() ?? undefined,
|
|
5597
5609
|
scopeId: deps.getScopeId?.()
|
|
5598
5610
|
}).catch(() => {});
|
|
5599
5611
|
}
|
|
@@ -6449,8 +6461,12 @@ ${options}
|
|
|
6449
6461
|
blockers: args.blockers,
|
|
6450
6462
|
estimatedMinutesRemaining: optionalNonNegativeNumberArg(args.estimatedMinutesRemaining, "estimatedMinutesRemaining"),
|
|
6451
6463
|
...mergedRecentActions && { recentActions: mergedRecentActions },
|
|
6452
|
-
...runActivity.length > 0 && { runActivity }
|
|
6464
|
+
...runActivity.length > 0 && { runActivity },
|
|
6465
|
+
implicitCreate: true
|
|
6453
6466
|
});
|
|
6467
|
+
if (result.session === null) {
|
|
6468
|
+
untrack(cardId, deps.getScopeId?.());
|
|
6469
|
+
}
|
|
6454
6470
|
return { success: true, midSessionLearnings: 0, ...result };
|
|
6455
6471
|
}
|
|
6456
6472
|
case "harmony_end_agent_session": {
|
|
@@ -6460,7 +6476,9 @@ ${options}
|
|
|
6460
6476
|
const endProgressPercent = optionalPercentArg(args.progressPercent, "progressPercent");
|
|
6461
6477
|
await flushMemoryActions(client3, cardId);
|
|
6462
6478
|
cleanupMemorySession(cardId);
|
|
6463
|
-
let result = {
|
|
6479
|
+
let result = {
|
|
6480
|
+
session: null
|
|
6481
|
+
};
|
|
6464
6482
|
let sessionEndError = null;
|
|
6465
6483
|
try {
|
|
6466
6484
|
result = await client3.endAgentSession(cardId, {
|
|
@@ -6510,6 +6528,29 @@ ${options}
|
|
|
6510
6528
|
const sessionId = z.string().min(1).parse(args.sessionId);
|
|
6511
6529
|
const sinceSeq = args.sinceSeq !== undefined ? z.number().int().min(0).parse(args.sinceSeq) : 0;
|
|
6512
6530
|
const result = await client3.getPendingMessages(cardId, sessionId, sinceSeq);
|
|
6531
|
+
const stoppedByHuman = result.session?.stoppedByHuman === true;
|
|
6532
|
+
const ended = result.session?.ended === true;
|
|
6533
|
+
if (ended || stoppedByHuman) {
|
|
6534
|
+
untrack(cardId, deps.getScopeId?.());
|
|
6535
|
+
}
|
|
6536
|
+
if (stoppedByHuman) {
|
|
6537
|
+
return {
|
|
6538
|
+
success: true,
|
|
6539
|
+
...result,
|
|
6540
|
+
stopped: true,
|
|
6541
|
+
stopReason: "human_stopped",
|
|
6542
|
+
instruction: "A human stopped this run — stop work now. Make no further edits, commits, pushes, or progress writes, and do not move the card. Commit any uncommitted work in place so it isn't lost, then report to the user what you completed, what is unfinished, and the branch holding it."
|
|
6543
|
+
};
|
|
6544
|
+
}
|
|
6545
|
+
if (ended) {
|
|
6546
|
+
return {
|
|
6547
|
+
success: true,
|
|
6548
|
+
...result,
|
|
6549
|
+
sessionStale: true,
|
|
6550
|
+
staleReason: result.session?.status,
|
|
6551
|
+
instruction: 'This session id is no longer live (it was closed as "' + (result.session?.status ?? "missing") + '") — but nobody stopped you. Do NOT abandon the work. Open a fresh session with harmony_start_agent_session, use its new id for later polls, and carry on.'
|
|
6552
|
+
};
|
|
6553
|
+
}
|
|
6513
6554
|
return { success: true, ...result };
|
|
6514
6555
|
}
|
|
6515
6556
|
case "harmony_generate_prompt": {
|
package/dist/lib/api-client.js
CHANGED
|
@@ -833,6 +833,14 @@ var init_prompt_builder = __esm(() => {
|
|
|
833
833
|
execute: `EXECUTE MODE: Implement this task completely. Write production-ready code following best practices. Include necessary tests and documentation.`
|
|
834
834
|
};
|
|
835
835
|
});
|
|
836
|
+
// ../harmony-shared/dist/agentStaleness.js
|
|
837
|
+
var AGENT_HEARTBEAT_LIVENESS_MS = 5 * 60 * 1000;
|
|
838
|
+
var AGENT_MILESTONE_LIVENESS_MS = 30 * 60 * 1000;
|
|
839
|
+
var AGENT_SWEEP_DAEMON_MS = 30 * 60 * 1000;
|
|
840
|
+
var AGENT_SWEEP_INTERACTIVE_MS = 2 * 60 * 60 * 1000;
|
|
841
|
+
var AGENT_SWEEP_PAUSED_MS = 4 * 60 * 60 * 1000;
|
|
842
|
+
var SWEPT_SESSION_WRITE_GRACE_MS = 60 * 60 * 1000;
|
|
843
|
+
var ACTIVE_STATUSES = new Set(["working", "blocked", "waiting"]);
|
|
836
844
|
// ../harmony-shared/dist/cardLinks.js
|
|
837
845
|
var LINK_TYPE_INVERSES = {
|
|
838
846
|
relates_to: "relates_to",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gethmy/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.21.0",
|
|
4
4
|
"description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"serve:remote": "bun src/remote.ts",
|
|
65
65
|
"dev": "bun --watch src/index.ts",
|
|
66
66
|
"test": "bun run test:unit && bun run test:integration",
|
|
67
|
-
"test:unit": "bun test src/__tests__/active-learning.test.ts src/__tests__/context-assembly.test.ts src/__tests__/prompt-builder.test.ts src/__tests__/memory-audit.test.ts src/__tests__/skills.test.ts src/__tests__/hmy-config.test.ts src/__tests__/tool-dispatch.test.ts src/__tests__/mcp-integration.test.ts src/__tests__/auto-session.test.ts src/__tests__/setup-confirm.test.ts",
|
|
67
|
+
"test:unit": "bun test src/__tests__/active-learning.test.ts src/__tests__/context-assembly.test.ts src/__tests__/prompt-builder.test.ts src/__tests__/memory-audit.test.ts src/__tests__/skills.test.ts src/__tests__/hmy-config.test.ts src/__tests__/tool-dispatch.test.ts src/__tests__/mcp-integration.test.ts src/__tests__/auto-session.test.ts src/__tests__/remote-routing.test.ts src/__tests__/setup-confirm.test.ts",
|
|
68
68
|
"test:integration": "bun test src/__tests__/integration-memory-system.test.ts src/__tests__/integration-memory-crud.test.ts",
|
|
69
69
|
"typecheck": "tsc --noEmit",
|
|
70
70
|
"prepublishOnly": "bun run typecheck && bun run build"
|
package/src/api-client.ts
CHANGED
|
@@ -513,6 +513,12 @@ export class HarmonyApiClient {
|
|
|
513
513
|
userId: string;
|
|
514
514
|
source: "api_key" | "oauth" | "jwt";
|
|
515
515
|
workspaceId: string | null;
|
|
516
|
+
/**
|
|
517
|
+
* The OAuth grant behind the credential — stable across refresh rotation,
|
|
518
|
+
* null for api_key/jwt auth (#774). Optional because this client may be
|
|
519
|
+
* talking to a harmony-api deployed before the field existed.
|
|
520
|
+
*/
|
|
521
|
+
grantId?: string | null;
|
|
516
522
|
}> {
|
|
517
523
|
return this.request("GET", "/auth/context");
|
|
518
524
|
}
|
|
@@ -1064,27 +1070,58 @@ export class HarmonyApiClient {
|
|
|
1064
1070
|
agentId?: string | null;
|
|
1065
1071
|
/** Advertise that this session polls for queued steering messages (card #473). */
|
|
1066
1072
|
steerable?: boolean;
|
|
1073
|
+
/**
|
|
1074
|
+
* The start is a side effect of a trigger tool (auto-session), not a
|
|
1075
|
+
* deliberate `/hmy` start. Barred on a card whose last session a human
|
|
1076
|
+
* cancelled, so a surviving client can't resurrect it (#770).
|
|
1077
|
+
*/
|
|
1078
|
+
implicitCreate?: boolean;
|
|
1067
1079
|
status?: "working" | "blocked" | "paused" | "completed";
|
|
1068
1080
|
progressPercent?: number;
|
|
1069
1081
|
currentTask?: string;
|
|
1070
1082
|
blockers?: string[];
|
|
1071
1083
|
estimatedMinutesRemaining?: number;
|
|
1072
1084
|
},
|
|
1073
|
-
): Promise<{
|
|
1085
|
+
): Promise<{
|
|
1086
|
+
session: unknown;
|
|
1087
|
+
created: boolean;
|
|
1088
|
+
stopped?: boolean;
|
|
1089
|
+
stopReason?: "human_stopped";
|
|
1090
|
+
}> {
|
|
1074
1091
|
return this.request("POST", `/cards/${cardId}/agent-context`, data);
|
|
1075
1092
|
}
|
|
1076
1093
|
|
|
1077
1094
|
/**
|
|
1078
|
-
* Drain queued `user_message` steering events for a session (card #473)
|
|
1095
|
+
* Drain queued `user_message` steering events for a session (card #473), and
|
|
1096
|
+
* learn whether that session is still live (card #770).
|
|
1097
|
+
*
|
|
1079
1098
|
* Wraps the existing `GET /cards/{id}/agent-messages` endpoint that the daemon
|
|
1080
1099
|
* already polls — exposed so MCP/human sessions (e.g. /hmy) can consume steering
|
|
1081
1100
|
* at their checkpoints. Returns messages with `seq > sinceSeq`, oldest first.
|
|
1101
|
+
*
|
|
1102
|
+
* `session.stoppedByHuman` is the durable half of Stop: the ephemeral
|
|
1103
|
+
* `agent_command` broadcast has no consumer for `agent_id == null` sessions, so
|
|
1104
|
+
* this poll is where such a client finds out it was stopped.
|
|
1105
|
+
*
|
|
1106
|
+
* `ended` is the weaker fact — the row is no longer live, most often because the
|
|
1107
|
+
* 30-minute stale cron closed it, which is NOT a stop. Only `stoppedByHuman` is
|
|
1108
|
+
* terminal. `session` is absent on an older server, or when the liveness read
|
|
1109
|
+
* itself failed; absent means "assume live".
|
|
1082
1110
|
*/
|
|
1083
1111
|
async getPendingMessages(
|
|
1084
1112
|
cardId: string,
|
|
1085
1113
|
sessionId: string,
|
|
1086
1114
|
sinceSeq: number,
|
|
1087
|
-
): Promise<{
|
|
1115
|
+
): Promise<{
|
|
1116
|
+
messages: unknown[];
|
|
1117
|
+
session?: {
|
|
1118
|
+
id: string;
|
|
1119
|
+
status: string;
|
|
1120
|
+
endedAt: string | null;
|
|
1121
|
+
ended: boolean;
|
|
1122
|
+
stoppedByHuman: boolean;
|
|
1123
|
+
};
|
|
1124
|
+
}> {
|
|
1088
1125
|
const params = new URLSearchParams();
|
|
1089
1126
|
params.set("sessionId", sessionId);
|
|
1090
1127
|
params.set("sinceSeq", String(sinceSeq));
|
|
@@ -1144,8 +1181,24 @@ export class HarmonyApiClient {
|
|
|
1144
1181
|
* report on a session but must never bring one back from the dead (#696).
|
|
1145
1182
|
*/
|
|
1146
1183
|
noCreate?: boolean;
|
|
1184
|
+
/**
|
|
1185
|
+
* This write only meant to REPORT progress — creating a session is a side
|
|
1186
|
+
* effect, not the intent. Weaker than `noCreate` (an agent that never called
|
|
1187
|
+
* `start_agent_session` still gets one), but permanently barred on a card
|
|
1188
|
+
* whose last session a human cancelled, where the #663 cooldown only
|
|
1189
|
+
* deferred the respawn by 10 minutes (#770). Reply carries `stopped: true`.
|
|
1190
|
+
*/
|
|
1191
|
+
implicitCreate?: boolean;
|
|
1147
1192
|
},
|
|
1148
|
-
): Promise<{
|
|
1193
|
+
): Promise<{
|
|
1194
|
+
session: unknown;
|
|
1195
|
+
created: boolean;
|
|
1196
|
+
/** Set when the write was refused because a human stopped this card's run. */
|
|
1197
|
+
stopped?: boolean;
|
|
1198
|
+
stopReason?: "human_stopped";
|
|
1199
|
+
/** How to legitimately work this card again (an explicit start). */
|
|
1200
|
+
recovery?: string;
|
|
1201
|
+
}> {
|
|
1149
1202
|
return this.request("POST", `/cards/${cardId}/agent-context`, data);
|
|
1150
1203
|
}
|
|
1151
1204
|
|
|
@@ -1184,7 +1237,18 @@ export class HarmonyApiClient {
|
|
|
1184
1237
|
attemptNumber?: number;
|
|
1185
1238
|
maxAttempts?: number;
|
|
1186
1239
|
},
|
|
1187
|
-
): Promise<{
|
|
1240
|
+
): Promise<{
|
|
1241
|
+
session: unknown;
|
|
1242
|
+
/**
|
|
1243
|
+
* Whether this call actually wrote a terminal state (#769). `false` — with
|
|
1244
|
+
* `session: null` — means the response succeeded but changed nothing: there
|
|
1245
|
+
* was no live session (`no_active_session`), or a concurrent caller ended
|
|
1246
|
+
* the same one first (`concurrently_ended`). Older servers omit both fields,
|
|
1247
|
+
* so treat `undefined` as "unknown" and fall back to `session !== null`.
|
|
1248
|
+
*/
|
|
1249
|
+
ended?: boolean;
|
|
1250
|
+
reason?: "no_active_session" | "concurrently_ended";
|
|
1251
|
+
}> {
|
|
1188
1252
|
return this.request("DELETE", `/cards/${cardId}/agent-context`, data);
|
|
1189
1253
|
}
|
|
1190
1254
|
|
package/src/auto-session.ts
CHANGED
|
@@ -130,10 +130,20 @@ const CHECK_INTERVAL_MS = 60 * 1000; // 60 seconds
|
|
|
130
130
|
* Bounding the heartbeat by real activity restores that backstop: once the
|
|
131
131
|
* agent stops calling harmony tools we stop beating, `updated_at` goes stale,
|
|
132
132
|
* and the cron closes the row as `failed`/`stale`. Sits deliberately BELOW the
|
|
133
|
-
* cron's
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
133
|
+
* cron's threshold so a stalled session can actually reach it, and well above
|
|
134
|
+
* the milestone-gap the heartbeat exists to bridge (#608). Activity revives it —
|
|
135
|
+
* `trackActivity` refreshes `lastActivityAt` on every card-scoped tool call.
|
|
136
|
+
*
|
|
137
|
+
* The cron's threshold for the interactive sessions this manages is **2 hours**,
|
|
138
|
+
* not the 30 minutes this comment originally cited (card #771 —
|
|
139
|
+
* `AGENT_SWEEP_INTERACTIVE_MS` in `packages/harmony-shared/src/agentStaleness.ts`;
|
|
140
|
+
* 30 minutes still applies to daemon runs, which heartbeat independently). The
|
|
141
|
+
* old pairing is what produced the bug: this window expiring at 25 minutes plus a
|
|
142
|
+
* flat 30-minute sweep gave a *working* agent only ~55 minutes of total silence,
|
|
143
|
+
* so one long verify/review phase got its live session closed as failed. Keeping
|
|
144
|
+
* this window short is still correct — it is what lets a finished-but-unclosed
|
|
145
|
+
* session be reaped at all — but the reaping now happens on a window sized for
|
|
146
|
+
* real work rather than one that a single gate run can outlast.
|
|
137
147
|
*/
|
|
138
148
|
export const HEARTBEAT_ACTIVITY_WINDOW_MS = 25 * 60 * 1000; // 25 minutes
|
|
139
149
|
|
|
@@ -274,13 +284,22 @@ export async function trackActivity(
|
|
|
274
284
|
await autoEndSession(scope, client, otherCardId, "completed");
|
|
275
285
|
}
|
|
276
286
|
|
|
277
|
-
// Start a new auto-session
|
|
287
|
+
// Start a new auto-session. `implicitCreate` because this start is a side
|
|
288
|
+
// effect of a trigger tool, not a deliberate `harmony_start_agent_session`: on
|
|
289
|
+
// a card whose last session a human cancelled, the server refuses it outright
|
|
290
|
+
// rather than letting a surviving client walk the run back to life one cooldown
|
|
291
|
+
// later (card #770). An explicit `/hmy` re-run is unaffected — it starts the
|
|
292
|
+
// session by name and only faces the 10-minute #663 window.
|
|
278
293
|
try {
|
|
279
|
-
await client.startAgentSession(cardId, {
|
|
294
|
+
const started = await client.startAgentSession(cardId, {
|
|
280
295
|
agentIdentifier,
|
|
281
296
|
agentName,
|
|
282
297
|
status: "working",
|
|
298
|
+
implicitCreate: true,
|
|
283
299
|
});
|
|
300
|
+
// Refused: don't track it locally either, or the sweep would heartbeat a
|
|
301
|
+
// session that does not exist and `checkInactivity` would later "end" it.
|
|
302
|
+
if (started?.session === null) return;
|
|
284
303
|
} catch {
|
|
285
304
|
// Session start failed (might already have one), still track locally
|
|
286
305
|
}
|