@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/src/server.ts
CHANGED
|
@@ -81,6 +81,11 @@ import {
|
|
|
81
81
|
unlinkedReport,
|
|
82
82
|
} from "./plan-task-link.js";
|
|
83
83
|
import { collectPlaybookMetricWarnings } from "./playbook-metric-warnings.js";
|
|
84
|
+
import {
|
|
85
|
+
beginHookTimeline,
|
|
86
|
+
endHookTimeline,
|
|
87
|
+
stopAllRunEventForwarders,
|
|
88
|
+
} from "./run-event-forwarder.js";
|
|
84
89
|
import { stripSkillPreamble } from "./skills.js";
|
|
85
90
|
|
|
86
91
|
// --- Signed-upload handshake (artifacts & card attachments) ---
|
|
@@ -2368,7 +2373,10 @@ export const TOOLS = {
|
|
|
2368
2373
|
"belongs to, and this sets the return leg the plan needs to show real progress instead of " +
|
|
2369
2374
|
"guessing from board-column names. Use it to repair a link, to re-point a criterion at a " +
|
|
2370
2375
|
"different card, or to mark a criterion `completed` once its card actually delivered it. " +
|
|
2371
|
-
"Leave a criterion the card did NOT deliver open — an open criterion is the signal."
|
|
2376
|
+
"Leave a criterion the card did NOT deliver open — an open criterion is the signal. " +
|
|
2377
|
+
"Linking a card that belongs to no plan yet also adopts it into this one (reported as " +
|
|
2378
|
+
"`cardPlanAdopted`), and linking a card that already belongs to a DIFFERENT plan is " +
|
|
2379
|
+
"refused — move it with harmony_update_card first, so the two directions cannot drift.",
|
|
2372
2380
|
inputSchema: {
|
|
2373
2381
|
type: "object",
|
|
2374
2382
|
properties: {
|
|
@@ -2381,7 +2389,9 @@ export const TOOLS = {
|
|
|
2381
2389
|
cardId: {
|
|
2382
2390
|
type: "string",
|
|
2383
2391
|
description:
|
|
2384
|
-
"Card that fulfils this criterion. Must live in the plan's own project
|
|
2392
|
+
"Card that fulfils this criterion. Must live in the plan's own project, and " +
|
|
2393
|
+
"must belong to this plan or to no plan yet — a card already in another plan " +
|
|
2394
|
+
"is refused rather than silently re-pointed.",
|
|
2385
2395
|
},
|
|
2386
2396
|
status: {
|
|
2387
2397
|
type: "string",
|
|
@@ -4263,6 +4273,16 @@ export async function handleToolCall(
|
|
|
4263
4273
|
deps.getScopeId?.(),
|
|
4264
4274
|
);
|
|
4265
4275
|
|
|
4276
|
+
// Publish the session on disk and start draining the `PostToolUse` hook's
|
|
4277
|
+
// spool (#874). This is what gives an MCP session a tool-call timeline
|
|
4278
|
+
// instead of one row per progress checkpoint. It no-ops on a daemon run
|
|
4279
|
+
// and on any failure — see `beginHookTimeline`.
|
|
4280
|
+
beginHookTimeline({
|
|
4281
|
+
cardId,
|
|
4282
|
+
agentSessionId,
|
|
4283
|
+
getClient: () => client,
|
|
4284
|
+
});
|
|
4285
|
+
|
|
4266
4286
|
return {
|
|
4267
4287
|
success: true,
|
|
4268
4288
|
assignedTo,
|
|
@@ -4377,6 +4397,11 @@ export async function handleToolCall(
|
|
|
4377
4397
|
await flushMemoryActions(client, cardId);
|
|
4378
4398
|
cleanupMemorySession(cardId);
|
|
4379
4399
|
|
|
4400
|
+
// Drain the hook spool and unpublish while the session row still accepts
|
|
4401
|
+
// appends (#874). Ordered before `endAgentSession` for that reason: the
|
|
4402
|
+
// last tool calls of a run are the ones a reader most wants.
|
|
4403
|
+
await endHookTimeline(cardId);
|
|
4404
|
+
|
|
4380
4405
|
// End the session — tolerate failure (e.g., session already ended or not found).
|
|
4381
4406
|
// Typed off the client so the `ended`/`reason` discriminator (#769) reaches the
|
|
4382
4407
|
// tool payload by contract, not by accident of the spread below. Left absent
|
|
@@ -5419,7 +5444,11 @@ export async function handleToolCall(
|
|
|
5419
5444
|
}
|
|
5420
5445
|
|
|
5421
5446
|
case "harmony_get_plan": {
|
|
5422
|
-
let result: {
|
|
5447
|
+
let result: {
|
|
5448
|
+
plan: unknown;
|
|
5449
|
+
tasks: unknown[];
|
|
5450
|
+
foreign_criteria?: unknown[];
|
|
5451
|
+
} | null = null;
|
|
5423
5452
|
|
|
5424
5453
|
if (args.planId) {
|
|
5425
5454
|
const planId = z.string().uuid().parse(args.planId);
|
|
@@ -5427,7 +5456,14 @@ export async function handleToolCall(
|
|
|
5427
5456
|
} else if (args.cardId) {
|
|
5428
5457
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
5429
5458
|
result = await client.getPlanByCardId(cardId);
|
|
5430
|
-
|
|
5459
|
+
// Three distinct answers, and they must stay distinct. The route
|
|
5460
|
+
// normalises "nothing to report" to `{plan: null, tasks: []}` rather
|
|
5461
|
+
// than a null body, so a truthiness check alone cannot tell the clean
|
|
5462
|
+
// no-plan card from the divergent one.
|
|
5463
|
+
if (
|
|
5464
|
+
!result ||
|
|
5465
|
+
(result.plan == null && !result.foreign_criteria?.length)
|
|
5466
|
+
) {
|
|
5431
5467
|
return {
|
|
5432
5468
|
success: true,
|
|
5433
5469
|
plan: null,
|
|
@@ -5435,6 +5471,27 @@ export async function handleToolCall(
|
|
|
5435
5471
|
message: "No plan linked to this card",
|
|
5436
5472
|
};
|
|
5437
5473
|
}
|
|
5474
|
+
// The card belongs to no plan, yet some criterion elsewhere names it (#1054).
|
|
5475
|
+
// Reporting that as "no plan linked" is the silence this card was filed for,
|
|
5476
|
+
// one level up from the daemon: a person asking which plan a card serves must
|
|
5477
|
+
// be told the two directions disagree, and which end to repair.
|
|
5478
|
+
// `result.foreign_criteria?.length` is the whole predicate: the route
|
|
5479
|
+
// always answers with an object, so `result.plan == null` is also true
|
|
5480
|
+
// for the ordinary "this card is in no plan" case. Without the second
|
|
5481
|
+
// half, every plan-less card was told plan criteria point at it and
|
|
5482
|
+
// handed a repair instruction for a problem it did not have — the
|
|
5483
|
+
// card's own failure mode inverted.
|
|
5484
|
+
if (result.plan == null && result.foreign_criteria?.length) {
|
|
5485
|
+
return {
|
|
5486
|
+
success: true,
|
|
5487
|
+
plan: null,
|
|
5488
|
+
tasks: [],
|
|
5489
|
+
divergentCriteria: result.foreign_criteria ?? [],
|
|
5490
|
+
message:
|
|
5491
|
+
"This card belongs to no plan, but plan criteria point at it. " +
|
|
5492
|
+
"Repair it with harmony_update_card (planId) or harmony_link_plan_task.",
|
|
5493
|
+
};
|
|
5494
|
+
}
|
|
5438
5495
|
} else {
|
|
5439
5496
|
throw new Error("Either planId or cardId must be provided");
|
|
5440
5497
|
}
|
|
@@ -5443,6 +5500,11 @@ export async function handleToolCall(
|
|
|
5443
5500
|
success: true,
|
|
5444
5501
|
plan: result.plan,
|
|
5445
5502
|
tasks: result.tasks,
|
|
5503
|
+
// Criteria naming this card from a plan it does not belong to — a data error,
|
|
5504
|
+
// reported rather than folded into `tasks` (#1054). Omitted when there are none.
|
|
5505
|
+
...(result.foreign_criteria?.length
|
|
5506
|
+
? { divergentCriteria: result.foreign_criteria }
|
|
5507
|
+
: {}),
|
|
5446
5508
|
};
|
|
5447
5509
|
}
|
|
5448
5510
|
|
|
@@ -5490,7 +5552,14 @@ export async function handleToolCall(
|
|
|
5490
5552
|
const found = findPlanTask(tasks, taskId);
|
|
5491
5553
|
if (!found.ok) throw new Error(found.reason);
|
|
5492
5554
|
|
|
5493
|
-
|
|
5555
|
+
// Linking also maintains the card's own `plan_id` server-side (#1054) — see the
|
|
5556
|
+
// "which direction carries the truth" note in `_shared/plan-task-card-scope.ts`.
|
|
5557
|
+
// A card that belongs to a DIFFERENT plan is refused there, so this call can throw
|
|
5558
|
+
// where it used to succeed and leave the two columns disagreeing.
|
|
5559
|
+
const result = await client.updatePlanTask(planId, taskId, {
|
|
5560
|
+
cardId,
|
|
5561
|
+
status,
|
|
5562
|
+
});
|
|
5494
5563
|
return {
|
|
5495
5564
|
success: true,
|
|
5496
5565
|
planTask: {
|
|
@@ -5500,6 +5569,9 @@ export async function handleToolCall(
|
|
|
5500
5569
|
...(cardId
|
|
5501
5570
|
? linkedReport(planId, found.task, cardId)
|
|
5502
5571
|
: { linked: found.task.card_id != null }),
|
|
5572
|
+
...(cardId && result.cardPlanAdopted
|
|
5573
|
+
? { cardPlanAdopted: true }
|
|
5574
|
+
: {}),
|
|
5503
5575
|
...(status ? { status } : {}),
|
|
5504
5576
|
},
|
|
5505
5577
|
};
|
|
@@ -5844,6 +5916,14 @@ export class HarmonyMCPServer {
|
|
|
5844
5916
|
} catch {
|
|
5845
5917
|
// Best-effort
|
|
5846
5918
|
}
|
|
5919
|
+
try {
|
|
5920
|
+
// Drain any tool calls the hook spooled but the timer had not posted,
|
|
5921
|
+
// and remove the on-disk pointers so a later hook cannot route to a
|
|
5922
|
+
// session this process took with it (#874).
|
|
5923
|
+
await stopAllRunEventForwarders();
|
|
5924
|
+
} catch {
|
|
5925
|
+
// Best-effort
|
|
5926
|
+
}
|
|
5847
5927
|
destroyAutoSession();
|
|
5848
5928
|
process.exit(exitCode);
|
|
5849
5929
|
};
|