@gethmy/mcp 3.7.0 → 3.9.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/src/tui/setup.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import { createHash } from "node:crypto";
2
3
  import {
3
4
  existsSync,
@@ -23,7 +24,11 @@ import {
23
24
  } from "../config.js";
24
25
  import { loginWithBrowser, type OAuthTokens } from "../oauth-login.js";
25
26
  import { onboardNewUser } from "../onboard.js";
26
- import { buildSkillFile, HARMONY_WORKFLOW_PROMPT } from "../skills.js";
27
+ import { buildSkillFile } from "../skills.js";
28
+ import {
29
+ HARMONY_AGENTS_SECTION,
30
+ renderWorkflowPrompt,
31
+ } from "./agent-instructions.js";
27
32
  import { type AgentId, detectAgents } from "./agents.js";
28
33
  import { confirmOrDefault, shouldAssumeYes } from "./confirm.js";
29
34
  import { runDocsStep } from "./docs.js";
@@ -61,11 +66,17 @@ export interface SetupOptions {
61
66
  * keep, especially under autonomous agent runs. New tools default to prompting
62
67
  * until someone classifies them here (safe failure mode). `--allow-all-tools`
63
68
  * overrides this with a blanket grant.
69
+ *
70
+ * Every name here must exist in the advertised `TOOLS` object in `server.ts`.
71
+ * `harmony_get_card_by_short_id` sat in this list for four minor versions after
72
+ * 2.14.0 dropped it, writing a permission rule for a tool that can never be
73
+ * offered — and the same dead name was being installed into AGENTS.md, where an
74
+ * agent read it and had to correct us. `setup-agent-files.test.ts` now fails on
75
+ * a name that is not advertised, in this list and in every generated file (#1124).
64
76
  */
65
- const SAFE_HARMONY_TOOLS = [
77
+ export const SAFE_HARMONY_TOOLS = [
66
78
  // Reads
67
79
  "harmony_get_card",
68
- "harmony_get_card_by_short_id",
69
80
  "harmony_search_cards",
70
81
  "harmony_get_board",
71
82
  "harmony_get_context",
@@ -77,12 +88,19 @@ const SAFE_HARMONY_TOOLS = [
77
88
  "harmony_get_comments",
78
89
  "harmony_get_plan",
79
90
  "harmony_list_plans",
91
+ "harmony_get_playbook",
92
+ "harmony_list_playbook",
80
93
  "harmony_get_agent_session",
94
+ // The checkpoint steering poll. Absent until #1124, so every checkpoint of
95
+ // every run raised a prompt for a read the workflow itself prescribes.
96
+ "harmony_get_pending_messages",
81
97
  "harmony_get_workspace_members",
82
98
  "harmony_list_agents",
83
99
  "harmony_resolve_links",
100
+ "harmony_suggest_relations",
84
101
  "harmony_recall",
85
102
  "harmony_memory_search",
103
+ "harmony_vault_index",
86
104
  "harmony_generate_prompt",
87
105
  // Routine writes
88
106
  "harmony_create_card",
@@ -91,6 +109,7 @@ const SAFE_HARMONY_TOOLS = [
91
109
  "harmony_assign_card",
92
110
  "harmony_create_subtask",
93
111
  "harmony_toggle_subtask",
112
+ "harmony_update_subtask",
94
113
  "harmony_add_label_to_card",
95
114
  "harmony_remove_label_from_card",
96
115
  "harmony_create_label",
@@ -98,6 +117,10 @@ const SAFE_HARMONY_TOOLS = [
98
117
  "harmony_update_comment",
99
118
  "harmony_add_link_to_card",
100
119
  "harmony_remove_link_from_card",
120
+ // The durable carrier for a PR link — the completion path calls it on every
121
+ // card that changed code, and it mirrors the card-link pair above.
122
+ "harmony_add_external_link",
123
+ "harmony_remove_external_link",
101
124
  "harmony_start_agent_session",
102
125
  "harmony_update_agent_progress",
103
126
  "harmony_end_agent_session",
@@ -112,6 +135,9 @@ const SAFE_HARMONY_TOOLS = [
112
135
  "harmony_remember",
113
136
  "harmony_relate",
114
137
  "harmony_update_memory",
138
+ // A ranking counter on a recalled memory. A write, not a read — it never
139
+ // deletes, hides or supersedes anything.
140
+ "harmony_recall_feedback",
115
141
  "harmony_process_command",
116
142
  "harmony_sync",
117
143
  ];
@@ -401,7 +427,8 @@ export async function resolveProjectSlug(
401
427
  export interface FileToWrite {
402
428
  path: string;
403
429
  content: string;
404
- type: "text" | "json" | "toml";
430
+ /** `markdown` merges a Harmony-owned section into a file the project owns. */
431
+ type: "text" | "json" | "toml" | "markdown";
405
432
  tomlSection?: string;
406
433
  mode?: number;
407
434
  }
@@ -512,64 +539,15 @@ async function getAgentFiles(
512
539
  }
513
540
 
514
541
  case "codex": {
515
- // AGENTS.md in project root (always local - project context)
516
- const agentsContent = `# Harmony Integration
517
-
518
- This project uses Harmony for task management. When working on tasks:
519
-
520
- ## Agent identity — always identify as yourself
521
-
522
- Every \`harmony_start_agent_session\` call passes \`agentIdentifier\` + \`agentName\`. **Use your own
523
- identity, never a hardcoded one from this file.** AGENTS.md is a cross-runtime convention file, so
524
- more than one kind of agent will read it; the board shows agents as teammates, and a session
525
- attributed to the wrong runtime misattributes the work in front of the whole team.
526
-
527
- - \`agentIdentifier\` — a stable kebab-case id for the runtime you actually are
528
- - \`agentName\` — its human-readable name
529
-
530
- Known values: \`claude-code\` / "Claude Code", \`codex\` / "OpenAI Codex", \`cursor\` / "Cursor",
531
- \`claude-desktop\` / "Claude Desktop". If you are a runtime not listed here, use your own name rather
532
- than borrowing the closest entry.
533
-
534
- ## Starting Work on a Card
535
-
536
- When given a card reference (e.g., #42 or a card name), follow this workflow:
537
-
538
- 1. Use \`harmony_get_card\` or \`harmony_search_cards\` to find the card
539
- 2. Move the card to "In Progress" using \`harmony_move_card\`
540
- 3. Add the "agent" label using \`harmony_add_label_to_card\`
541
- 4. Start a session with \`harmony_start_agent_session\`, passing **your own** \`agentIdentifier\` +
542
- \`agentName\` (see "Agent identity" above)
543
- 5. Show the card details to the user
544
- 6. Use \`harmony_generate_prompt\` to get guidance, then implement the solution
545
- 7. Update progress periodically with \`harmony_update_agent_progress\`
546
- 8. When done, call \`harmony_end_agent_session\` and move to "Review"
547
-
548
- ## Auto-Detect Card for Implementation Tasks
549
-
550
- Before implementing a plan or feature, check if it maps to an existing Harmony card:
551
-
552
- 1. Use \`harmony_search_cards\` with keywords from the task description
553
- 2. If a match is found, call \`harmony_start_agent_session\` with **your own** \`agentIdentifier\` +
554
- \`agentName\` (see "Agent identity" above), plus \`moveToColumn: "In Progress"\`, \`addLabels: ["agent"]\`
555
- 3. Update progress with \`harmony_update_agent_progress\` at milestones
556
- 4. When done, call \`harmony_end_agent_session\` with status: "completed", moveToColumn: "Review"
557
-
558
- Skip if: work was already started with a card reference, or no matching card exists.
559
-
560
- ## Available Harmony Tools
561
-
562
- - \`harmony_get_card\`, \`harmony_get_card_by_short_id\`, \`harmony_search_cards\` - Find cards
563
- - \`harmony_move_card\` - Move cards between columns
564
- - \`harmony_add_label_to_card\`, \`harmony_remove_label_from_card\` - Manage labels
565
- - \`harmony_start_agent_session\`, \`harmony_update_agent_progress\`, \`harmony_end_agent_session\` - Track work
566
- - \`harmony_get_board\` - Get board state
567
- - \`harmony_generate_prompt\` - Get role-based guidance and focus areas for the card
568
- `;
542
+ // The Harmony section of AGENTS.md, written as type "markdown" so it is
543
+ // MERGED into whatever the project already has: the docs-step scaffold
544
+ // written earlier in this same run, or a file the user maintains by hand.
545
+ // A plain text write destroyed both setup forces writes on a fresh
546
+ // install, and this push lands after the scaffold's (#1124).
569
547
  files.push({
570
548
  path: join(cwd, "AGENTS.md"),
571
- content: agentsContent,
572
- type: "text",
549
+ content: HARMONY_AGENTS_SECTION,
550
+ type: "markdown",
573
551
  });
574
552
 
575
553
  // Codex prompt file
@@ -582,7 +560,7 @@ arguments:
582
560
  required: true
583
561
  ---
584
562
 
585
- ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "{{card}}").replace("Your agent identifier", "codex").replace("Your agent name", "OpenAI Codex")}
563
+ ${renderWorkflowPrompt({ cardArgument: "{{card}}", agentIdentifier: "codex", agentName: "OpenAI Codex" })}
586
564
  `;
587
565
 
588
566
  if (installMode === "global") {
@@ -652,7 +630,7 @@ alwaysApply: false
652
630
 
653
631
  When the user asks you to work on a Harmony card (references like #42, card names, or UUIDs):
654
632
 
655
- ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Your agent identifier", "cursor").replace("Your agent name", "Cursor AI")}
633
+ ${renderWorkflowPrompt({ cardArgument: "the card reference", agentIdentifier: "cursor", agentName: "Cursor" })}
656
634
  `;
657
635
 
658
636
  if (installMode === "global") {
@@ -708,7 +686,7 @@ description: Activate when user asks to work on a Harmony card (references like
708
686
 
709
687
  When working on a Harmony card:
710
688
 
711
- ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Your agent identifier", "windsurf").replace("Your agent name", "Windsurf AI")}
689
+ ${renderWorkflowPrompt({ cardArgument: "the card reference", agentIdentifier: "windsurf", agentName: "Windsurf" })}
712
690
  `;
713
691
 
714
692
  if (installMode === "global") {
@@ -1576,6 +1554,11 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
1576
1554
  ` ${colors.success("\u2713")} ${colors.dim(formatPath(writtenLocalConfigPath, home))} ${colors.dim("(created)")}`,
1577
1555
  );
1578
1556
 
1557
+ // The scan runs AFTER the global mirror below, deliberately: it spawns a
1558
+ // child with inherited stdio for as long as this repo's test suite takes,
1559
+ // and a Ctrl-C there must not cost the operator the global default that
1560
+ // every server started outside this directory reads (#893).
1561
+ //
1579
1562
  // Mirror the choice into the GLOBAL default, explicitly (#893). The local
1580
1563
  // file above already pins this directory; this second write is what gives a
1581
1564
  // server started elsewhere — Claude Desktop, another repo — a context at
@@ -1592,6 +1575,8 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
1592
1575
  { global: true },
1593
1576
  );
1594
1577
  }
1578
+
1579
+ await offerCommandScan(dirname(writtenLocalConfigPath), assumeYes);
1595
1580
  }
1596
1581
 
1597
1582
  // Step 10: Show completion message
@@ -1686,3 +1671,99 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
1686
1671
  }
1687
1672
  console.log("");
1688
1673
  }
1674
+
1675
+ /**
1676
+ * The command that proposes a repo's `commands` block by running it.
1677
+ *
1678
+ * `@latest` is this repo's own convention for an `npx` spec (`@gethmy/mcp@latest`
1679
+ * in every documented invocation) and it is load-bearing here: `scan-commands`
1680
+ * arrives in a release later than several already published, and a warm npx
1681
+ * cache of an older `@gethmy/agent` would otherwise hand the operator the
1682
+ * "may predate it" fallback forever with no way to see why.
1683
+ */
1684
+ const SCAN_ARGV = ["--yes", "@gethmy/agent@latest", "scan-commands"] as const;
1685
+
1686
+ function scanTip(): void {
1687
+ console.log(
1688
+ ` ${colors.dim("Tip: run")} ${colors.highlight(`npx ${SCAN_ARGV.slice(1).join(" ")}`)} ${colors.dim("to have the daemon prove which build, test and dev commands this repo has, and write them into the pin.")}`,
1689
+ );
1690
+ }
1691
+
1692
+ /**
1693
+ * Offer to scan this repo's commands, and run the scanner when asked (#1116).
1694
+ *
1695
+ * Card 1116's first acceptance criterion is that the scanner RUNS when a
1696
+ * project is set up — this is that moment, and a printed tip is not a run.
1697
+ * What this package must not do is scan by itself: the scan executes the
1698
+ * repo's own build, test and dev commands, and `@gethmy/mcp` neither depends
1699
+ * on `@gethmy/harness` (which owns both the execution and the schema) nor
1700
+ * spawns a toolchain anywhere else. So it does what the tip asked a person to
1701
+ * do — spawn the agent CLI — which keeps the dependency direction intact
1702
+ * (agent → harness → shared, mcp beside them) and re-types no schema.
1703
+ *
1704
+ * Three properties, each deliberate.
1705
+ *
1706
+ * **Asked, never assumed.** The scan starts this repo's build, test and dev
1707
+ * commands, which is minutes of work and arbitrary code from the checkout. A
1708
+ * person is present at setup and is the right one to say yes.
1709
+ *
1710
+ * **Non-interactive setups are not scanned at all** — `--yes`, a pipe, a
1711
+ * coding agent — even though `--yes` means yes to everything else. Those two
1712
+ * are not the same question: the other confirmations write files this command
1713
+ * already owns, and this one hands control to another package's process for
1714
+ * an unbounded time. They get the tip, i.e. exactly the behaviour that
1715
+ * shipped before this.
1716
+ *
1717
+ * **A failure is not an error.** The published `@gethmy/agent` gains this
1718
+ * subcommand only on its next release, and `npx` needs a network. Either way
1719
+ * the fallback is the tip, so an older agent degrades to the previous
1720
+ * behaviour rather than turning setup red over an optional convenience.
1721
+ *
1722
+ * It deliberately does NOT pass `--write`: the proposal is printed for a
1723
+ * person to read, which is card 1116's fourth criterion (never applied
1724
+ * silently), and `scan-commands --write` is the separate, explicit act.
1725
+ */
1726
+ export async function offerCommandScan(
1727
+ repoDir: string,
1728
+ assumeYes: boolean,
1729
+ ): Promise<void> {
1730
+ if (assumeYes) {
1731
+ scanTip();
1732
+ return;
1733
+ }
1734
+
1735
+ const scan = await confirmOrDefault(assumeYes, {
1736
+ message:
1737
+ "Scan this repo's commands now? It runs your build, test and dev scripts once to prove which ones exist. Nothing is written.",
1738
+ initialValue: true,
1739
+ });
1740
+ // Ctrl-C ends setup, as it does at every other prompt in this file. A
1741
+ // cancel folded into "no" would carry on through the remaining steps as if
1742
+ // the person had answered, which is the one reading they did not give.
1743
+ if (p.isCancel(scan)) {
1744
+ p.cancel("Setup cancelled.");
1745
+ process.exit(0);
1746
+ }
1747
+ if (!scan) {
1748
+ scanTip();
1749
+ return;
1750
+ }
1751
+
1752
+ console.log(
1753
+ ` ${colors.dim("Running")} ${colors.highlight(`npx ${SCAN_ARGV.slice(1).join(" ")}`)}${colors.dim(" …")}`,
1754
+ );
1755
+ const result = spawnSync("npx", [...SCAN_ARGV], {
1756
+ cwd: repoDir,
1757
+ stdio: "inherit",
1758
+ });
1759
+ if (result.error || result.status !== 0) {
1760
+ console.log(
1761
+ ` ${colors.dim("The scan did not run — your installed @gethmy/agent may predate it.")}`,
1762
+ );
1763
+ scanTip();
1764
+ return;
1765
+ }
1766
+ console.log(
1767
+ ` ${colors.dim("Re-run it with")} ${colors.highlight("--write")} ${colors.dim("to merge that block into the pin.")}`,
1768
+ );
1769
+ }
package/src/tui/writer.ts CHANGED
@@ -204,6 +204,112 @@ export function appendToToml(
204
204
  }
205
205
  }
206
206
 
207
+ /**
208
+ * Markers delimiting the Harmony-owned section of a shared markdown file.
209
+ * Everything between them is package-generated and refreshed on reinstall;
210
+ * everything outside them belongs to the project and is never rewritten.
211
+ */
212
+ export const MARKDOWN_SECTION_START = "<!-- harmony:start -->";
213
+ export const MARKDOWN_SECTION_END = "<!-- harmony:end -->";
214
+
215
+ /**
216
+ * Locate the Harmony-owned section: the first WELL-FORMED marker pair — a
217
+ * START, the first END after it, and no second START in between.
218
+ *
219
+ * **This choice is the whole safety argument of `mergeMarkdownSection`,** and
220
+ * the two obvious rules are each unsafe in one direction. First-START-first-END
221
+ * swallows everything between a stray START above our section and our real END.
222
+ * Last-START-first-END-after does the mirror: a project quoting both markers
223
+ * BELOW our section loses whatever sits between those two mentions. A file that
224
+ * merely shows the markers inside a code fence hits one or the other.
225
+ *
226
+ * Requiring the span to hold no START defeats both, because a stray marker
227
+ * disqualifies the candidate it sits inside and the scan moves past it. What
228
+ * this does NOT do is notice a file holding two complete sections: it updates
229
+ * the first and leaves the second stale. That is a duplicate to spot by eye,
230
+ * not content destroyed, which is the trade this function exists to make.
231
+ */
232
+ function findSection(text: string): { start: number; end: number } | null {
233
+ let from = 0;
234
+ while (true) {
235
+ const start = text.indexOf(MARKDOWN_SECTION_START, from);
236
+ if (start === -1) return null;
237
+ const bodyFrom = start + MARKDOWN_SECTION_START.length;
238
+ const end = text.indexOf(MARKDOWN_SECTION_END, bodyFrom);
239
+ if (end === -1) return null;
240
+ const nextStart = text.indexOf(MARKDOWN_SECTION_START, bodyFrom);
241
+ if (nextStart === -1 || nextStart > end) return { start, end };
242
+ // `start` is unpaired. `nextStart > from` always, so the scan terminates.
243
+ from = nextStart;
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Merge a Harmony-owned section into a shared markdown file (AGENTS.md).
249
+ *
250
+ * `AGENTS.md` is the project's file, not ours. It may already carry the
251
+ * docs-step scaffold written earlier in the same run, or a file the user has
252
+ * maintained by hand for months. A plain `writeFile` destroyed both: setup runs
253
+ * with `force: true` on every fresh install (`needsSkills`), which bypasses the
254
+ * exists-skip, and the agent files are pushed onto `allFiles` AFTER the docs
255
+ * scaffold — so the last write of `AGENTS.md` won and the scaffold vanished in
256
+ * the same breath as the line announcing it. Hence markers and a merge (#1124).
257
+ *
258
+ * - File absent → create it holding just the section.
259
+ * - A section is present (see `findSection`) → replace what lies between its
260
+ * markers, and only under `force` (matching `appendToToml`) — the section is
261
+ * package-generated, so a reinstall is when it should be refreshed.
262
+ * - No section → append. The file's existing content is never rewritten.
263
+ */
264
+ export function mergeMarkdownSection(
265
+ filePath: string,
266
+ content: string,
267
+ options: WriteOptions = {},
268
+ ): FileResult {
269
+ const section = `${MARKDOWN_SECTION_START}\n${content.trim()}\n${MARKDOWN_SECTION_END}\n`;
270
+
271
+ if (!existsSync(filePath)) {
272
+ try {
273
+ ensureDir(dirname(filePath));
274
+ writeFileSync(filePath, section, { mode: 0o644 });
275
+ return { path: filePath, action: "create" };
276
+ } catch (error) {
277
+ return {
278
+ path: filePath,
279
+ action: "skip",
280
+ error: error instanceof Error ? error.message : String(error),
281
+ };
282
+ }
283
+ }
284
+
285
+ try {
286
+ const existing = readFileSync(filePath, "utf-8");
287
+ const found = findSection(existing);
288
+
289
+ if (found) {
290
+ if (!options.force) return { path: filePath, action: "skip" };
291
+ const updated =
292
+ existing.slice(0, found.start) +
293
+ section.trimEnd() +
294
+ existing.slice(found.end + MARKDOWN_SECTION_END.length);
295
+ writeFileSync(filePath, updated, { mode: 0o644 });
296
+ return { path: filePath, action: "update" };
297
+ }
298
+
299
+ // No section we can identify — every byte of this file is the project's.
300
+ // Append, never overwrite, however forceful the caller is.
301
+ const separator = existing.endsWith("\n") ? "\n" : "\n\n";
302
+ writeFileSync(filePath, existing + separator + section, { mode: 0o644 });
303
+ return { path: filePath, action: "merge" };
304
+ } catch (error) {
305
+ return {
306
+ path: filePath,
307
+ action: "skip",
308
+ error: error instanceof Error ? error.message : String(error),
309
+ };
310
+ }
311
+ }
312
+
207
313
  /**
208
314
  * Write multiple files with progress display
209
315
  */
@@ -211,7 +317,7 @@ export async function writeFilesWithProgress(
211
317
  files: Array<{
212
318
  path: string;
213
319
  content: string;
214
- type: "text" | "json" | "toml";
320
+ type: "text" | "json" | "toml" | "markdown";
215
321
  jsonKey?: string;
216
322
  tomlSection?: string;
217
323
  mode?: number;
@@ -232,6 +338,8 @@ export async function writeFilesWithProgress(
232
338
  result = mergeJsonFile(file.path, jsonContent, options);
233
339
  } else if (file.type === "toml" && file.tomlSection) {
234
340
  result = appendToToml(file.path, file.tomlSection, file.content, options);
341
+ } else if (file.type === "markdown") {
342
+ result = mergeMarkdownSection(file.path, file.content, options);
235
343
  } else {
236
344
  result = writeFile(file.path, file.content, {
237
345
  ...options,
@@ -256,7 +364,15 @@ export async function writeFilesWithProgress(
256
364
  } else if (result.action === "skip") {
257
365
  console.log(messages.fileSkipped(displayPath));
258
366
  } else {
259
- const actionLabel = result.action === "merge" ? "updated" : "created";
367
+ // `update` used to fall through to "created" harmless while it was rare,
368
+ // and a lie the moment AGENTS.md started taking the merge path on every
369
+ // reinstall, telling the user a months-old file was just created (#1124).
370
+ const actionLabel =
371
+ result.action === "create"
372
+ ? "created"
373
+ : result.action === "merge"
374
+ ? "merged"
375
+ : "updated";
260
376
  console.log(
261
377
  ` ${colors.success("\u2713")} ${colors.dim(displayPath)} ${colors.dim(`(${actionLabel})`)}`,
262
378
  );