@gethmy/mcp 3.8.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/dist/cli.js CHANGED
@@ -18,8 +18,9 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
18
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
19
 
20
20
  // src/config.ts
21
+ import { execFileSync } from "node:child_process";
21
22
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
22
- import { homedir } from "node:os";
23
+ import { homedir, tmpdir } from "node:os";
23
24
  import { dirname, join, parse, resolve } from "node:path";
24
25
  function noteLegacyConfigDir(path) {
25
26
  if (warnedLegacyConfigDir)
@@ -36,6 +37,23 @@ function noteLegacyLocalPin(path) {
36
37
  function noteLocalPinRename(from, to) {
37
38
  console.error(`Harmony: wrote the repo pin to ${to} (the pre-#1082 ${from} is now ignored and can be deleted).`);
38
39
  }
40
+ function noteUntrackedLocalPin(path) {
41
+ if (warnedUntrackedLocalPin)
42
+ return;
43
+ warnedUntrackedLocalPin = true;
44
+ console.error(`Harmony: ${path} is ignored by git, so it will not travel with a branch — ` + `a fresh clone, and every worktree the agent daemon cuts, will not have it. ` + `Commit it if you want it to describe this repo everywhere.`);
45
+ }
46
+ function isGitIgnored(path) {
47
+ try {
48
+ execFileSync("git", ["check-ignore", "--quiet", path], {
49
+ cwd: dirname(path),
50
+ stdio: "ignore"
51
+ });
52
+ return true;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
39
57
  function getHmyRootDir() {
40
58
  return join(homedir(), CONFIG_DIR_NAME);
41
59
  }
@@ -150,19 +168,96 @@ function saveLocalConfig(config, cwd) {
150
168
  if (foundPath !== null && foundPath !== localConfigPath) {
151
169
  noteLocalPinRename(foundPath, localConfigPath);
152
170
  }
153
- const existingConfig = loadLocalConfig(cwd) || {
154
- workspaceId: null,
155
- projectId: null
156
- };
157
- const newConfig = { ...existingConfig, ...config };
158
- const cleanConfig = {};
159
- if (newConfig.workspaceId)
160
- cleanConfig.workspaceId = newConfig.workspaceId;
161
- if (newConfig.projectId)
162
- cleanConfig.projectId = newConfig.projectId;
163
- writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
171
+ const existing = readRawLocalConfig(foundPath ?? localConfigPath);
172
+ const merged = { ...existing };
173
+ if ("workspaceId" in config) {
174
+ if (config.workspaceId)
175
+ merged.workspaceId = config.workspaceId;
176
+ else
177
+ delete merged.workspaceId;
178
+ }
179
+ if ("projectId" in config) {
180
+ if (config.projectId)
181
+ merged.projectId = config.projectId;
182
+ else
183
+ delete merged.projectId;
184
+ }
185
+ writeFileSync(localConfigPath, `${JSON.stringify(merged, null, localIndent(localConfigPath))}
186
+ `);
187
+ if (isGitIgnored(localConfigPath))
188
+ noteUntrackedLocalPin(localConfigPath);
164
189
  return localConfigPath;
165
190
  }
191
+ function readRawLocalConfig(path) {
192
+ let text;
193
+ try {
194
+ text = readFileSync(path, "utf-8");
195
+ } catch {
196
+ return {};
197
+ }
198
+ try {
199
+ const parsed = JSON.parse(text);
200
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
201
+ return parsed;
202
+ }
203
+ } catch {}
204
+ noteUnparsableLocalPin(path, text);
205
+ return {};
206
+ }
207
+ function noteUnparsableLocalPin(path, contents) {
208
+ let backup = null;
209
+ try {
210
+ backup = join(tmpdir(), `hmy-pin-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json.bak`);
211
+ writeFileSync(backup, contents);
212
+ } catch {
213
+ backup = null;
214
+ }
215
+ console.error(`Harmony: ${path} could not be parsed as JSON, so the pin write REPLACED it. ` + (backup ? `The previous contents are in ${backup}.` : "The previous contents could not be backed up and are gone.") + ` If it carried a "commands" block, re-add it.`);
216
+ }
217
+ function localIndent(configPath) {
218
+ const root = dirname(configPath);
219
+ for (const name of ["biome.json", "biome.jsonc"]) {
220
+ const parsed = readJsonish(join(root, name));
221
+ if (!parsed || typeof parsed !== "object")
222
+ continue;
223
+ const formatter = parsed.formatter;
224
+ if (formatter?.indentStyle === "space") {
225
+ const width = formatter.indentWidth;
226
+ return typeof width === "number" && width > 0 ? width : 2;
227
+ }
228
+ return "\t";
229
+ }
230
+ const editorconfig = readTextSafely(join(root, ".editorconfig"));
231
+ if (editorconfig) {
232
+ if (/^\s*indent_style\s*=\s*tab\s*$/im.test(editorconfig))
233
+ return "\t";
234
+ const size = editorconfig.match(/^\s*indent_size\s*=\s*(\d+)\s*$/im);
235
+ if (size) {
236
+ const width = Number(size[1]);
237
+ if (width > 0)
238
+ return width;
239
+ }
240
+ }
241
+ return 2;
242
+ }
243
+ function readJsonish(path) {
244
+ const text = readTextSafely(path);
245
+ if (text === null)
246
+ return null;
247
+ try {
248
+ const stripped = text.replace(/^\s*\/\/.*$/gm, "").replace(/,(\s*[}\]])/g, "$1");
249
+ return JSON.parse(stripped);
250
+ } catch {
251
+ return null;
252
+ }
253
+ }
254
+ function readTextSafely(path) {
255
+ try {
256
+ return readFileSync(path, "utf-8");
257
+ } catch {
258
+ return null;
259
+ }
260
+ }
166
261
  function hasLocalConfig(cwd) {
167
262
  return findLocalConfigPath(cwd) !== null;
168
263
  }
@@ -296,7 +391,7 @@ function getMemoryDir() {
296
391
  return config.memoryDir;
297
392
  return join(homedir(), ".harmony", "memory");
298
393
  }
299
- var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".hmy.json", LEGACY_LOCAL_CONFIG_FILENAME = ".harmony-mcp.json", CONFIG_DIR_NAME = ".hmy", CONFIG_DIR_SUBDIR = "agent", LEGACY_CONFIG_DIR_NAME = ".harmony-mcp", warnedLegacyConfigDir = false, warnedLegacyLocalPin = false;
394
+ var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".hmy.json", LEGACY_LOCAL_CONFIG_FILENAME = ".harmony-mcp.json", CONFIG_DIR_NAME = ".hmy", CONFIG_DIR_SUBDIR = "agent", LEGACY_CONFIG_DIR_NAME = ".harmony-mcp", warnedLegacyConfigDir = false, warnedLegacyLocalPin = false, warnedUntrackedLocalPin = false;
300
395
  var init_config = () => {};
301
396
 
302
397
  // src/prompt-builder.ts
@@ -726,7 +821,7 @@ var init_prompt_builder = __esm(() => {
726
821
  };
727
822
  VARIANT_INSTRUCTIONS = {
728
823
  analysis: `ANALYSIS MODE: Analyze this task thoroughly. Identify requirements, constraints, edge cases, and potential challenges. Do NOT implement anything yet - focus on understanding and planning.`,
729
- draft: `DRAFT MODE: Create a detailed implementation plan with code structure, key decisions, and approach. Include pseudocode or skeleton code where helpful. This is for review before full implementation.`,
824
+ draft: `DRAFT MODE: Draft the approach for review before implementing. Cover the key decisions with their reasons, the data model and the API contracts, and success criteria a test can check. A short signature or schema sketch is fine wherever an interpretation gap would otherwise remain; function bodies, control flow and test code are not - an implementer transcribes a plan faithfully, defects included.`,
730
825
  execute: `EXECUTE MODE: Implement this task completely. Write production-ready code following best practices. Include necessary tests and documentation.`
731
826
  };
732
827
  });
@@ -1037,7 +1132,7 @@ __export(exports_run_state, {
1037
1132
  RUN_STATE_DIR_ENV: () => RUN_STATE_DIR_ENV,
1038
1133
  MAX_POINTER_AGE_MS: () => MAX_POINTER_AGE_MS
1039
1134
  });
1040
- import { execFileSync } from "node:child_process";
1135
+ import { execFileSync as execFileSync2 } from "node:child_process";
1041
1136
  import {
1042
1137
  existsSync as existsSync3,
1043
1138
  mkdirSync as mkdirSync3,
@@ -1094,7 +1189,7 @@ function psParentTable() {
1094
1189
  return psTableCache;
1095
1190
  const table = new Map;
1096
1191
  try {
1097
- const out = execFileSync("ps", ["-Ao", "pid=,ppid="], {
1192
+ const out = execFileSync2("ps", ["-Ao", "pid=,ppid="], {
1098
1193
  encoding: "utf-8",
1099
1194
  timeout: 2000,
1100
1195
  stdio: ["ignore", "pipe", "ignore"]
@@ -4686,81 +4781,6 @@ function parseHmyConfig(text) {
4686
4781
  }
4687
4782
 
4688
4783
  // src/skills.ts
4689
- var HARMONY_WORKFLOW_PROMPT = `# Harmony Card Workflow
4690
-
4691
- Start work on a Harmony card. Card reference: $ARGUMENTS
4692
-
4693
- ## 1. Find & Fetch Card
4694
-
4695
- Parse the reference and fetch the card:
4696
- - \`#42\` or \`42\` → \`harmony_get_card\` with \`shortId: 42\`
4697
- - UUID → \`harmony_get_card\` with \`cardId\`
4698
- - Name/text → \`harmony_search_cards\` with \`query\`
4699
-
4700
- ## 2. Get Board State
4701
-
4702
- Call \`harmony_get_board\` to get columns and labels. From the response:
4703
- - Find the "In Progress" (or "Progress") column ID
4704
- - Find the "agent" label ID
4705
-
4706
- ## 3. Setup Card for Work
4707
-
4708
- Execute these in sequence:
4709
- 1. \`harmony_move_card\` → Move to "In Progress" column
4710
- 2. \`harmony_add_label_to_card\` → Add "agent" label
4711
- 3. \`harmony_start_agent_session\`:
4712
- - \`cardId\`: Card UUID
4713
- - \`agentIdentifier\`: Your agent identifier
4714
- - \`agentName\`: Your agent name
4715
- - \`currentTask\`: "Analyzing card requirements"
4716
-
4717
- ## 4. Generate Work Prompt
4718
-
4719
- Call \`harmony_generate_prompt\` with:
4720
- - \`cardId\` or \`shortId\` (+ \`projectId\` if using shortId)
4721
- - \`variant\`: Select based on task:
4722
- - \`"execute"\` (default) → Clear tasks, bug fixes, well-defined work
4723
- - \`"analysis"\` → Complex features, unclear requirements
4724
- - \`"draft"\` → Medium complexity, want feedback first
4725
-
4726
- The generated prompt provides role framing, focus areas, subtasks, linked cards, and suggested outputs.
4727
-
4728
- ## 5. Display Card Summary
4729
-
4730
- Show the user: Card title, short ID, role, priority, labels, due date, description, and subtasks.
4731
-
4732
- ## 6. Implement Solution
4733
-
4734
- Work on the card following the generated prompt's guidance. Update progress at milestones:
4735
- - \`harmony_update_agent_progress\` with \`progressPercent\` (0-100), \`currentTask\`, \`status\`, \`blockers\`
4736
-
4737
- **Progress checkpoints:** 20% (exploration), 50% (implementation), 80% (testing), 100% (done)
4738
-
4739
- ## 7. Complete Work
4740
-
4741
- When finished:
4742
- 1. \`harmony_end_agent_session\` with \`status: "completed"\`, \`progressPercent: 100\`
4743
- 2. \`harmony_move_card\` to "Review" column
4744
- 3. Summarize accomplishments
4745
-
4746
- If pausing: \`harmony_end_agent_session\` with \`status: "paused"\`
4747
-
4748
- ## Key Tools Reference
4749
-
4750
- **Cards:** \`harmony_get_card\` (by \`cardId\`, \`shortId\`, or \`shortIds\`), \`harmony_search_cards\`, \`harmony_create_card\`, \`harmony_update_card\`, \`harmony_move_card\`, \`harmony_delete_card\`, \`harmony_assign_card\`
4751
-
4752
- **Subtasks:** \`harmony_create_subtask\`, \`harmony_toggle_subtask\`, \`harmony_delete_subtask\`
4753
-
4754
- **Labels:** \`harmony_add_label_to_card\`, \`harmony_remove_label_from_card\`, \`harmony_create_label\`
4755
-
4756
- **Links:** \`harmony_add_link_to_card\`, \`harmony_remove_link_from_card\`, \`harmony_get_card_links\`
4757
-
4758
- **Board:** \`harmony_get_board\`, \`harmony_list_projects\`, \`harmony_get_context\`, \`harmony_set_project_context\`
4759
-
4760
- **Sessions:** \`harmony_start_agent_session\`, \`harmony_update_agent_progress\`, \`harmony_end_agent_session\`, \`harmony_get_agent_session\`
4761
-
4762
- **AI:** \`harmony_generate_prompt\`, \`harmony_process_command\`
4763
- `;
4764
4784
  function buildSkillFile(skill) {
4765
4785
  const content = stripSkillPreamble(skill.content);
4766
4786
  if (skill.skillVersion !== undefined && !hasMetadataVersion(content)) {
@@ -6615,7 +6635,7 @@ var TOOLS = {
6615
6635
  }
6616
6636
  },
6617
6637
  harmony_create_plan: {
6618
- description: "Create a new project plan. Use this to upload implementation plans created during planning. Returns a URL where the plan can be viewed and edited in Harmony.",
6638
+ description: "Create a new project plan. Use this to upload a plan written during planning. Returns a URL where the plan can be viewed and edited in Harmony.",
6619
6639
  inputSchema: {
6620
6640
  type: "object",
6621
6641
  properties: {
@@ -6638,7 +6658,10 @@ var TOOLS = {
6638
6658
  items: {
6639
6659
  type: "object",
6640
6660
  properties: {
6641
- content: { type: "string", description: "Task description" },
6661
+ content: {
6662
+ type: "string",
6663
+ description: 'One success criterion, as a statement about the finished product that a test can check ("the mirror matches the migration chain"), never a work package ("write the mirror script"). One criterion may take several cards.'
6664
+ },
6642
6665
  priority: {
6643
6666
  type: "string",
6644
6667
  enum: ["high", "medium", "low"],
@@ -6652,7 +6675,7 @@ var TOOLS = {
6652
6675
  },
6653
6676
  required: ["content"]
6654
6677
  },
6655
- description: "Optional list of tasks to create with the plan"
6678
+ description: "The plan's success criteria, one entry each - what must be true when the plan is done, not a breakdown of the work to do it."
6656
6679
  }
6657
6680
  },
6658
6681
  required: ["title"]
@@ -6673,7 +6696,7 @@ var TOOLS = {
6673
6696
  }
6674
6697
  },
6675
6698
  harmony_update_plan: {
6676
- description: "Update an existing plan. Can update title, content, or status.",
6699
+ description: "Update an existing plan: its title, content, status, or the timeline dates its bar spans. " + "`startDate`/`endDate` are the plan's OWN schedule, the same pair a person sets by dragging the bar in the timeline view. " + "A plan is pinned on both or on neither: send both to schedule it, or both as null to return it to the span derived from its linked cards. " + "Sending one alone is refused unless the plan is already pinned. " + "They are never adjusted automatically — a card running past `endDate` is drawn as an overrun, and only a person extends the plan.",
6677
6700
  inputSchema: {
6678
6701
  type: "object",
6679
6702
  properties: {
@@ -6687,6 +6710,16 @@ var TOOLS = {
6687
6710
  type: "string",
6688
6711
  enum: ["draft", "active", "archived"],
6689
6712
  description: "New status"
6713
+ },
6714
+ startDate: {
6715
+ type: "string",
6716
+ nullable: true,
6717
+ description: "Timeline start as YYYY-MM-DD, or null to unpin (send endDate null too)."
6718
+ },
6719
+ endDate: {
6720
+ type: "string",
6721
+ nullable: true,
6722
+ description: "Timeline end as YYYY-MM-DD, or null to unpin (send startDate null too). Must not precede startDate."
6690
6723
  }
6691
6724
  },
6692
6725
  required: ["planId"]
@@ -8671,6 +8704,13 @@ ${options}
8671
8704
  if (args.status !== undefined) {
8672
8705
  updates.status = z.enum(["draft", "active", "archived"]).parse(args.status);
8673
8706
  }
8707
+ const planDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
8708
+ message: "expected a date as YYYY-MM-DD"
8709
+ });
8710
+ if (args.startDate !== undefined)
8711
+ updates.startDate = planDate.nullable().parse(args.startDate);
8712
+ if (args.endDate !== undefined)
8713
+ updates.endDate = planDate.nullable().parse(args.endDate);
8674
8714
  const result = await client3.updatePlan(planId, updates);
8675
8715
  return { success: true, plan: result.plan };
8676
8716
  }
@@ -8970,6 +9010,7 @@ class HarmonyMCPServer {
8970
9010
  }
8971
9011
 
8972
9012
  // src/tui/setup.ts
9013
+ import { spawnSync } from "node:child_process";
8973
9014
  import { createHash as createHash5 } from "node:crypto";
8974
9015
  import {
8975
9016
  existsSync as existsSync9,
@@ -8984,6 +9025,288 @@ import * as p4 from "@clack/prompts";
8984
9025
  init_config();
8985
9026
  init_oauth_login();
8986
9027
 
9028
+ // src/tui/agent-instructions.ts
9029
+ var HARMONY_PLAN_RULE = `**A plan says what must be true and why. It does not contain the code.** In it: the architecture
9030
+ and technology decisions with their reasons, the data model and the API contracts, a short
9031
+ signature or schema sketch wherever an interpretation gap would otherwise remain, and success
9032
+ criteria a test can check. Not in it: function bodies, control flow, error handling, test code.
9033
+ Detail follows risk — a throwaway script gets a rough plan, while auth, money, migrations and
9034
+ anything security-relevant get their contracts and edge cases written out.
9035
+
9036
+ Code in a plan carries the authority of a plan and the quality of a draft that no compiler, test
9037
+ or review has read, and an implementer transcribes it faithfully, defects included. So when the
9038
+ plan and the code disagree, the code is the evidence: check it, record the decision as a comment,
9039
+ and correct the plan as well as the code.`;
9040
+ var HARMONY_AGENTS_SECTION = `## Harmony
9041
+
9042
+ This project uses Harmony for task management. The \`harmony_*\` MCP tools are in your tool
9043
+ listing with live schemas — read them there. This section covers only what the schemas do not say.
9044
+
9045
+ ### Identify as yourself
9046
+
9047
+ Every session call takes \`agentIdentifier\` + \`agentName\`. Use your OWN, never a value copied
9048
+ from this file. The board shows agents as teammates, so a session attributed to the wrong runtime
9049
+ misattributes the work in front of the whole team.
9050
+
9051
+ Known values: \`claude-code\` / "Claude Code" · \`codex\` / "OpenAI Codex" · \`cursor\` / "Cursor" ·
9052
+ \`claude-desktop\` / "Claude Desktop". If you are none of these, use your own name.
9053
+
9054
+ ### Starting work — one call, not three
9055
+
9056
+ \`harmony_start_agent_session\` moves the card and adds the labels itself. Do not call
9057
+ \`harmony_move_card\` or \`harmony_add_label_to_card\` first, and do not fetch the board for a
9058
+ column id or a label id — both arguments match by name.
9059
+
9060
+ \`\`\`
9061
+ harmony_start_agent_session({
9062
+ cardId,
9063
+ agentIdentifier, agentName, // your own
9064
+ currentTask: "Reading the auth middleware to find the affected routes",
9065
+ moveToColumn: "In Progress",
9066
+ addLabels: ["agent"],
9067
+ steerable: true, // only if you will poll for steering — see below
9068
+ })
9069
+ \`\`\`
9070
+
9071
+ **Then read the reply, because the setup half fails quietly.** \`movedTo\` names the column it
9072
+ actually moved to and \`labelsAdded\` the labels it actually added; a miss leaves them null or
9073
+ empty and raises no error. The column match is a case-insensitive **substring**, so a board with
9074
+ "Ready for Review" ahead of "Review" can take the wrong one. If \`movedTo\` is null or not the
9075
+ column you meant, call \`harmony_move_card\` — it matches exactly first and fails loudly, listing
9076
+ the columns.
9077
+
9078
+ Keep the returned \`session.id\`; the steering poll needs it. Then call
9079
+ \`harmony_generate_prompt\` for role framing and focus areas — \`variant\` is \`execute\`
9080
+ (default), \`analysis\`, or \`draft\`.
9081
+
9082
+ ### Progress — \`actions\` is what survives as evidence
9083
+
9084
+ On the card itself, \`progressPercent\` and \`currentTask\` each overwrite one field, so the live
9085
+ status shows only your latest checkpoint. The timeline keeps more: a checkpoint that carries both
9086
+ a \`progressPercent\` and a \`currentTask\` different from the last one leaves a row saying what you
9087
+ were **about to do**, and **each entry in \`actions\` leaves a row saying what you actually did**.
9088
+ Report four checkpoints with no \`actions\` and a two-hour run reads as four intentions and no
9089
+ evidence.
9090
+
9091
+ Name what you DID since the last checkpoint: the file you edited and why, the gate you ran and
9092
+ what it said, the approach you ruled out and on what evidence.
9093
+
9094
+ \`\`\`
9095
+ harmony_update_agent_progress({
9096
+ cardId, agentIdentifier, agentName,
9097
+ progressPercent: 50,
9098
+ currentTask: "Extracting refreshIfExpired() in auth.ts",
9099
+ actions: [
9100
+ { description: "Read auth.ts and middleware/session.ts — the refresh path is duplicated in both, which is the actual bug" },
9101
+ { description: "Ruled out patching verifyToken(): three routes depend on its current behaviour" },
9102
+ { description: "Ran bun run lint — green, exit 0" },
9103
+ ],
9104
+ })
9105
+ \`\`\`
9106
+
9107
+ Three to six entries per checkpoint, one sentence each; past 512 characters an entry is silently truncated. Facts, not
9108
+ intentions — one vague entry is worse than none. Checkpoints: 20% explored · 50% implementing ·
9109
+ 80% verifying · 100% done. \`currentTask\` is what you are doing now — never leave it generic.
9110
+
9111
+ ### Steering and Stop
9112
+
9113
+ If you passed \`steerable: true\`, poll right after every progress update:
9114
+
9115
+ \`\`\`
9116
+ harmony_get_pending_messages({ cardId, sessionId, sinceSeq }) // sinceSeq starts at 0
9117
+ \`\`\`
9118
+
9119
+ Messages come back oldest first. Fold them into the next step and advance \`sinceSeq\` to the
9120
+ largest \`seq\` returned, so each is handled exactly once.
9121
+
9122
+ Two flags come back and mean opposite things:
9123
+
9124
+ | flag | meaning | what to do |
9125
+ |---|---|---|
9126
+ | \`stopped: true\` | a human pressed Stop | **Terminal.** Make no further edits, commits, pushes, card moves, comments or progress writes. Report what is finished and where any uncommitted work lives. |
9127
+ | \`sessionStale: true\` | your session id is no longer live — usually the inactivity sweep | **Nobody stopped you.** Carry on — with a new id. |
9128
+
9129
+ \`harmony_update_agent_progress\` reports the same two flags, and the recovery from a stale session
9130
+ differs by which call told you:
9131
+
9132
+ - From the **progress** call, a replacement session has already been opened for you and inherited
9133
+ the steering channel. Take the new \`session.id\` from that reply and keep going.
9134
+ - From the **poll**, nothing was opened. Call \`harmony_start_agent_session\` yourself and poll
9135
+ with the id it returns.
9136
+
9137
+ If the two flags ever disagree, the stop wins.
9138
+
9139
+ ### Finishing
9140
+
9141
+ \`\`\`
9142
+ harmony_end_agent_session({ cardId, status: "completed", progressPercent: 100, moveToColumn: "Review" })
9143
+ \`\`\`
9144
+
9145
+ One call: it moves the card, and on \`status: "completed"\` it also removes the \`agent\` label. Use
9146
+ \`status: "paused"\` when you stop mid-flight — that leaves the label on, which is what you want.
9147
+
9148
+ Attach a PR **after** the session end has moved the card, both ways: \`harmony_add_external_link\`
9149
+ (durable — it survives a later description edit) and a \`PR: <url>\` line in the description.
9150
+
9151
+ ### Writing a plan
9152
+
9153
+ ${HARMONY_PLAN_RULE}
9154
+
9155
+ ### Traps
9156
+
9157
+ - **A \`shortId\` is project-scoped, and the two ways it can miss are opposites.** With **no**
9158
+ active project the number is resolved across every project you can reach, so it may come back
9159
+ \`needsDisambiguation\` with \`candidates\` — ask which one is meant. With an active project the
9160
+ number resolves only inside it, and a card that lives elsewhere fails with an error naming the
9161
+ projects it is really in — switch with \`harmony_set_project_context\` or pass \`projectId\`.
9162
+ Either way, check \`resolvedProject\` matches the card you meant before starting work.
9163
+ - **Fetch many cards in one call:** \`harmony_get_card({ shortIds: [400, 401, 402] })\`, max 100.
9164
+ - **Moving a card to a terminal column ends your session** — a column that marks cards done, or
9165
+ one named \`done\`, \`completed\` or \`review\`. The response says \`sessionEnded\`.
9166
+ - **Read \`harmony_get_comments\` before you act.** Steering messages do not include comments, and
9167
+ a later comment outranks an earlier one it contradicts.
9168
+ - **Report findings and decisions as comments, not description edits.** \`harmony_add_comment\`
9169
+ takes a \`commentType\`: \`question\` and \`blocker\` signal that you need a human; \`decision\`,
9170
+ \`finding\`, \`summary\`, \`progress\` and \`message\` are the rest.`;
9171
+ var HARMONY_WORKFLOW_PROMPT = `# Harmony Card Workflow
9172
+
9173
+ Work a Harmony card. Card reference: $ARGUMENTS
9174
+
9175
+ The \`harmony_*\` MCP tools are in your tool listing with live schemas — read them there. This
9176
+ prompt covers only what the schemas do not say.
9177
+
9178
+ ## 1. Fetch the card
9179
+
9180
+ - \`#42\` or \`42\` → \`harmony_get_card({ shortId: 42 })\`
9181
+ - UUID → \`harmony_get_card({ cardId })\`
9182
+ - A name or phrase → \`harmony_search_cards({ query })\`
9183
+ - Several at once → \`harmony_get_card({ shortIds: [40, 41, 42] })\`, max 100
9184
+
9185
+ A \`shortId\` is project-scoped, and the two ways it can miss are opposites. With **no** active
9186
+ project the number is resolved across every project you can reach, so it may come back
9187
+ \`needsDisambiguation\` with \`candidates\` — ask which one is meant. With an active project the
9188
+ number resolves only inside it, and a card that lives elsewhere fails with an error naming the
9189
+ projects it is really in; switch with \`harmony_set_project_context\` or pass \`projectId\`.
9190
+ Either way, check \`resolvedProject\` before you start.
9191
+
9192
+ Read \`harmony_get_comments\` too: a later comment outranks an earlier one it contradicts.
9193
+
9194
+ ## 2. Start the session — one call, not three
9195
+
9196
+ \`harmony_start_agent_session\` moves the card and adds the labels itself. Do not call
9197
+ \`harmony_move_card\` or \`harmony_add_label_to_card\` first, and do not fetch the board for a
9198
+ column id or a label id — both arguments match by name.
9199
+
9200
+ \`\`\`
9201
+ harmony_start_agent_session({
9202
+ cardId,
9203
+ agentIdentifier: "$AGENT_IDENTIFIER",
9204
+ agentName: "$AGENT_NAME",
9205
+ currentTask: "Reading the auth middleware to find the affected routes",
9206
+ moveToColumn: "In Progress",
9207
+ addLabels: ["agent"],
9208
+ steerable: true,
9209
+ })
9210
+ \`\`\`
9211
+
9212
+ \`currentTask\` says what you are about to do, specifically. Never "Analyzing card requirements".
9213
+ Keep the returned \`session.id\` — step 4 needs it.
9214
+
9215
+ **Read the reply, because the setup half fails quietly.** \`movedTo\` names the column it actually
9216
+ moved to and \`labelsAdded\` the labels it actually added; a miss leaves them null or empty and
9217
+ raises no error. The column match is a case-insensitive **substring**, so a board with "Ready for
9218
+ Review" ahead of "Review" can take the wrong one. If \`movedTo\` is null or not the column you
9219
+ meant, call \`harmony_move_card\` — it matches exactly first and fails loudly, listing the columns.
9220
+
9221
+ ## 3. Get the work prompt
9222
+
9223
+ \`harmony_generate_prompt\` with \`cardId\` (or \`shortId\` plus \`projectId\`) and a \`variant\`:
9224
+ \`execute\` (default) for well-defined work, \`analysis\` for unclear requirements, \`draft\` when
9225
+ you want feedback on a design first. It returns role framing, focus areas, subtasks and links.
9226
+
9227
+ Then show the user the card: title, short id, priority, labels, due date, description, subtasks.
9228
+
9229
+ ## 4. Implement, and check in at every milestone
9230
+
9231
+ Checkpoints: 20% explored · 50% implementing · 80% verifying · 100% done.
9232
+
9233
+ On the card itself, \`progressPercent\` and \`currentTask\` each overwrite one field, so the live
9234
+ status shows only your latest checkpoint. The timeline keeps more: a checkpoint that carries both
9235
+ a \`progressPercent\` and a \`currentTask\` different from the last one leaves a row saying what you
9236
+ were **about to do**, and **each entry in \`actions\` leaves a row saying what you actually did** —
9237
+ that is the evidence the team can still read afterwards.
9238
+
9239
+ \`\`\`
9240
+ harmony_update_agent_progress({
9241
+ cardId, agentIdentifier: "$AGENT_IDENTIFIER", agentName: "$AGENT_NAME",
9242
+ progressPercent: 50,
9243
+ currentTask: "Extracting refreshIfExpired() in auth.ts",
9244
+ actions: [
9245
+ { description: "Read auth.ts and middleware/session.ts — the refresh path is duplicated in both, which is the actual bug" },
9246
+ { description: "Ruled out patching verifyToken(): three routes depend on its current behaviour" },
9247
+ { description: "Ran bun run lint — green, exit 0" },
9248
+ ],
9249
+ status: "working", // or blocked / waiting / paused
9250
+ blockers: [],
9251
+ })
9252
+ \`\`\`
9253
+
9254
+ Three to six entries per checkpoint, one sentence each; past 512 characters an entry is silently truncated. Say what you did,
9255
+ not what you intend to do.
9256
+
9257
+ Right after each update, poll for steering:
9258
+
9259
+ \`\`\`
9260
+ harmony_get_pending_messages({ cardId, sessionId, sinceSeq }) // sinceSeq starts at 0
9261
+ \`\`\`
9262
+
9263
+ Fold any messages into the next step and advance \`sinceSeq\` to the largest \`seq\` returned. Two
9264
+ flags come back and mean opposite things:
9265
+
9266
+ - \`stopped: true\` — a human pressed Stop. **Terminal.** Make no further edits, commits, pushes,
9267
+ card moves, comments or progress writes; report what is finished and where any uncommitted work
9268
+ lives.
9269
+ - \`sessionStale: true\` — your session id is no longer live, usually the inactivity sweep. **Nobody stopped you.**
9270
+ Carry on, with a new id: the **poll** opens nothing, so call \`harmony_start_agent_session\`
9271
+ yourself; the **progress** call has already opened a replacement that inherited the steering
9272
+ channel, so just take the new \`session.id\` from its reply.
9273
+
9274
+ If the two flags ever disagree, the stop wins.
9275
+
9276
+ Report findings and decisions with \`harmony_add_comment\` (\`commentType\`: \`question\` and
9277
+ \`blocker\` signal that you need a human; \`decision\`, \`finding\`, \`summary\`, \`progress\`,
9278
+ \`message\`), not by editing the card description.
9279
+
9280
+ ## 5. Finish
9281
+
9282
+ \`\`\`
9283
+ harmony_end_agent_session({ cardId, status: "completed", progressPercent: 100, moveToColumn: "Review" })
9284
+ \`\`\`
9285
+
9286
+ One call: it moves the card, and on \`status: "completed"\` it also removes the \`agent\` label. Use
9287
+ \`status: "paused"\` when you stop mid-flight — that leaves the label on, which is what you want.
9288
+
9289
+ Opened a PR? Attach it **after** the session end has moved the card, both ways:
9290
+ \`harmony_add_external_link\` (durable — it survives a later description edit) and a
9291
+ \`PR: <url>\` line in the description.
9292
+
9293
+ Then summarise what changed.
9294
+
9295
+ ## Writing a plan
9296
+
9297
+ ${HARMONY_PLAN_RULE}
9298
+
9299
+ ## Worth knowing
9300
+
9301
+ - Moving a card to a terminal column ends your session — a column that marks cards done, or one
9302
+ named \`done\`, \`completed\` or \`review\`. The response says \`sessionEnded\`.
9303
+ - \`harmony_add_label_to_card\` and \`harmony_start_agent_session\`'s \`addLabels\` both CREATE a
9304
+ label that does not exist yet. Check the spelling.
9305
+ - \`harmony_add_comment\` works on any card you can see, including one you hold no session on.`;
9306
+ function renderWorkflowPrompt(opts) {
9307
+ return HARMONY_WORKFLOW_PROMPT.replaceAll("$ARGUMENTS", opts.cardArgument).replaceAll("$AGENT_IDENTIFIER", opts.agentIdentifier).replaceAll("$AGENT_NAME", opts.agentName);
9308
+ }
9309
+
8987
9310
  // src/tui/agents.ts
8988
9311
  import { existsSync as existsSync6 } from "node:fs";
8989
9312
  import { homedir as homedir5 } from "node:os";
@@ -9883,6 +10206,67 @@ function appendToToml(filePath, section, content, options = {}) {
9883
10206
  };
9884
10207
  }
9885
10208
  }
10209
+ var MARKDOWN_SECTION_START = "<!-- harmony:start -->";
10210
+ var MARKDOWN_SECTION_END = "<!-- harmony:end -->";
10211
+ function findSection(text) {
10212
+ let from = 0;
10213
+ while (true) {
10214
+ const start = text.indexOf(MARKDOWN_SECTION_START, from);
10215
+ if (start === -1)
10216
+ return null;
10217
+ const bodyFrom = start + MARKDOWN_SECTION_START.length;
10218
+ const end = text.indexOf(MARKDOWN_SECTION_END, bodyFrom);
10219
+ if (end === -1)
10220
+ return null;
10221
+ const nextStart = text.indexOf(MARKDOWN_SECTION_START, bodyFrom);
10222
+ if (nextStart === -1 || nextStart > end)
10223
+ return { start, end };
10224
+ from = nextStart;
10225
+ }
10226
+ }
10227
+ function mergeMarkdownSection(filePath, content, options = {}) {
10228
+ const section = `${MARKDOWN_SECTION_START}
10229
+ ${content.trim()}
10230
+ ${MARKDOWN_SECTION_END}
10231
+ `;
10232
+ if (!existsSync8(filePath)) {
10233
+ try {
10234
+ ensureDir(dirname3(filePath));
10235
+ writeFileSync5(filePath, section, { mode: 420 });
10236
+ return { path: filePath, action: "create" };
10237
+ } catch (error) {
10238
+ return {
10239
+ path: filePath,
10240
+ action: "skip",
10241
+ error: error instanceof Error ? error.message : String(error)
10242
+ };
10243
+ }
10244
+ }
10245
+ try {
10246
+ const existing = readFileSync7(filePath, "utf-8");
10247
+ const found = findSection(existing);
10248
+ if (found) {
10249
+ if (!options.force)
10250
+ return { path: filePath, action: "skip" };
10251
+ const updated = existing.slice(0, found.start) + section.trimEnd() + existing.slice(found.end + MARKDOWN_SECTION_END.length);
10252
+ writeFileSync5(filePath, updated, { mode: 420 });
10253
+ return { path: filePath, action: "update" };
10254
+ }
10255
+ const separator = existing.endsWith(`
10256
+ `) ? `
10257
+ ` : `
10258
+
10259
+ `;
10260
+ writeFileSync5(filePath, existing + separator + section, { mode: 420 });
10261
+ return { path: filePath, action: "merge" };
10262
+ } catch (error) {
10263
+ return {
10264
+ path: filePath,
10265
+ action: "skip",
10266
+ error: error instanceof Error ? error.message : String(error)
10267
+ };
10268
+ }
10269
+ }
9886
10270
  async function writeFilesWithProgress(files, options = {}) {
9887
10271
  const results = [];
9888
10272
  const home = homedir6();
@@ -9895,6 +10279,8 @@ async function writeFilesWithProgress(files, options = {}) {
9895
10279
  result = mergeJsonFile(file.path, jsonContent, options);
9896
10280
  } else if (file.type === "toml" && file.tomlSection) {
9897
10281
  result = appendToToml(file.path, file.tomlSection, file.content, options);
10282
+ } else if (file.type === "markdown") {
10283
+ result = mergeMarkdownSection(file.path, file.content, options);
9898
10284
  } else {
9899
10285
  result = writeFile(file.path, file.content, {
9900
10286
  ...options,
@@ -9912,7 +10298,7 @@ async function writeFilesWithProgress(files, options = {}) {
9912
10298
  } else if (result.action === "skip") {
9913
10299
  console.log(messages.fileSkipped(displayPath));
9914
10300
  } else {
9915
- const actionLabel = result.action === "merge" ? "updated" : "created";
10301
+ const actionLabel = result.action === "create" ? "created" : result.action === "merge" ? "merged" : "updated";
9916
10302
  console.log(` ${colors.success("✓")} ${colors.dim(displayPath)} ${colors.dim(`(${actionLabel})`)}`);
9917
10303
  }
9918
10304
  }
@@ -9940,7 +10326,6 @@ function getWriteSummary(files, options = {}) {
9940
10326
  // src/tui/setup.ts
9941
10327
  var SAFE_HARMONY_TOOLS = [
9942
10328
  "harmony_get_card",
9943
- "harmony_get_card_by_short_id",
9944
10329
  "harmony_search_cards",
9945
10330
  "harmony_get_board",
9946
10331
  "harmony_get_context",
@@ -9952,12 +10337,17 @@ var SAFE_HARMONY_TOOLS = [
9952
10337
  "harmony_get_comments",
9953
10338
  "harmony_get_plan",
9954
10339
  "harmony_list_plans",
10340
+ "harmony_get_playbook",
10341
+ "harmony_list_playbook",
9955
10342
  "harmony_get_agent_session",
10343
+ "harmony_get_pending_messages",
9956
10344
  "harmony_get_workspace_members",
9957
10345
  "harmony_list_agents",
9958
10346
  "harmony_resolve_links",
10347
+ "harmony_suggest_relations",
9959
10348
  "harmony_recall",
9960
10349
  "harmony_memory_search",
10350
+ "harmony_vault_index",
9961
10351
  "harmony_generate_prompt",
9962
10352
  "harmony_create_card",
9963
10353
  "harmony_update_card",
@@ -9965,6 +10355,7 @@ var SAFE_HARMONY_TOOLS = [
9965
10355
  "harmony_assign_card",
9966
10356
  "harmony_create_subtask",
9967
10357
  "harmony_toggle_subtask",
10358
+ "harmony_update_subtask",
9968
10359
  "harmony_add_label_to_card",
9969
10360
  "harmony_remove_label_from_card",
9970
10361
  "harmony_create_label",
@@ -9972,6 +10363,8 @@ var SAFE_HARMONY_TOOLS = [
9972
10363
  "harmony_update_comment",
9973
10364
  "harmony_add_link_to_card",
9974
10365
  "harmony_remove_link_from_card",
10366
+ "harmony_add_external_link",
10367
+ "harmony_remove_external_link",
9975
10368
  "harmony_start_agent_session",
9976
10369
  "harmony_update_agent_progress",
9977
10370
  "harmony_end_agent_session",
@@ -9984,6 +10377,7 @@ var SAFE_HARMONY_TOOLS = [
9984
10377
  "harmony_remember",
9985
10378
  "harmony_relate",
9986
10379
  "harmony_update_memory",
10380
+ "harmony_recall_feedback",
9987
10381
  "harmony_process_command",
9988
10382
  "harmony_sync"
9989
10383
  ];
@@ -10197,63 +10591,10 @@ ${summary}`);
10197
10591
  break;
10198
10592
  }
10199
10593
  case "codex": {
10200
- const agentsContent = `# Harmony Integration
10201
-
10202
- This project uses Harmony for task management. When working on tasks:
10203
-
10204
- ## Agent identity — always identify as yourself
10205
-
10206
- Every \`harmony_start_agent_session\` call passes \`agentIdentifier\` + \`agentName\`. **Use your own
10207
- identity, never a hardcoded one from this file.** AGENTS.md is a cross-runtime convention file, so
10208
- more than one kind of agent will read it; the board shows agents as teammates, and a session
10209
- attributed to the wrong runtime misattributes the work in front of the whole team.
10210
-
10211
- - \`agentIdentifier\` — a stable kebab-case id for the runtime you actually are
10212
- - \`agentName\` — its human-readable name
10213
-
10214
- Known values: \`claude-code\` / "Claude Code", \`codex\` / "OpenAI Codex", \`cursor\` / "Cursor",
10215
- \`claude-desktop\` / "Claude Desktop". If you are a runtime not listed here, use your own name rather
10216
- than borrowing the closest entry.
10217
-
10218
- ## Starting Work on a Card
10219
-
10220
- When given a card reference (e.g., #42 or a card name), follow this workflow:
10221
-
10222
- 1. Use \`harmony_get_card\` or \`harmony_search_cards\` to find the card
10223
- 2. Move the card to "In Progress" using \`harmony_move_card\`
10224
- 3. Add the "agent" label using \`harmony_add_label_to_card\`
10225
- 4. Start a session with \`harmony_start_agent_session\`, passing **your own** \`agentIdentifier\` +
10226
- \`agentName\` (see "Agent identity" above)
10227
- 5. Show the card details to the user
10228
- 6. Use \`harmony_generate_prompt\` to get guidance, then implement the solution
10229
- 7. Update progress periodically with \`harmony_update_agent_progress\`
10230
- 8. When done, call \`harmony_end_agent_session\` and move to "Review"
10231
-
10232
- ## Auto-Detect Card for Implementation Tasks
10233
-
10234
- Before implementing a plan or feature, check if it maps to an existing Harmony card:
10235
-
10236
- 1. Use \`harmony_search_cards\` with keywords from the task description
10237
- 2. If a match is found, call \`harmony_start_agent_session\` with **your own** \`agentIdentifier\` +
10238
- \`agentName\` (see "Agent identity" above), plus \`moveToColumn: "In Progress"\`, \`addLabels: ["agent"]\`
10239
- 3. Update progress with \`harmony_update_agent_progress\` at milestones
10240
- 4. When done, call \`harmony_end_agent_session\` with status: "completed", moveToColumn: "Review"
10241
-
10242
- Skip if: work was already started with a card reference, or no matching card exists.
10243
-
10244
- ## Available Harmony Tools
10245
-
10246
- - \`harmony_get_card\`, \`harmony_get_card_by_short_id\`, \`harmony_search_cards\` - Find cards
10247
- - \`harmony_move_card\` - Move cards between columns
10248
- - \`harmony_add_label_to_card\`, \`harmony_remove_label_from_card\` - Manage labels
10249
- - \`harmony_start_agent_session\`, \`harmony_update_agent_progress\`, \`harmony_end_agent_session\` - Track work
10250
- - \`harmony_get_board\` - Get board state
10251
- - \`harmony_generate_prompt\` - Get role-based guidance and focus areas for the card
10252
- `;
10253
10594
  files.push({
10254
10595
  path: join9(cwd, "AGENTS.md"),
10255
- content: agentsContent,
10256
- type: "text"
10596
+ content: HARMONY_AGENTS_SECTION,
10597
+ type: "markdown"
10257
10598
  });
10258
10599
  const promptContent = `---
10259
10600
  name: hmy
@@ -10264,7 +10605,7 @@ arguments:
10264
10605
  required: true
10265
10606
  ---
10266
10607
 
10267
- ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "{{card}}").replace("Your agent identifier", "codex").replace("Your agent name", "OpenAI Codex")}
10608
+ ${renderWorkflowPrompt({ cardArgument: "{{card}}", agentIdentifier: "codex", agentName: "OpenAI Codex" })}
10268
10609
  `;
10269
10610
  if (installMode === "global") {
10270
10611
  files.push({
@@ -10321,7 +10662,7 @@ alwaysApply: false
10321
10662
 
10322
10663
  When the user asks you to work on a Harmony card (references like #42, card names, or UUIDs):
10323
10664
 
10324
- ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Your agent identifier", "cursor").replace("Your agent name", "Cursor AI")}
10665
+ ${renderWorkflowPrompt({ cardArgument: "the card reference", agentIdentifier: "cursor", agentName: "Cursor" })}
10325
10666
  `;
10326
10667
  if (installMode === "global") {
10327
10668
  files.push({
@@ -10366,7 +10707,7 @@ description: Activate when user asks to work on a Harmony card (references like
10366
10707
 
10367
10708
  When working on a Harmony card:
10368
10709
 
10369
- ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Your agent identifier", "windsurf").replace("Your agent name", "Windsurf AI")}
10710
+ ${renderWorkflowPrompt({ cardArgument: "the card reference", agentIdentifier: "windsurf", agentName: "Windsurf" })}
10370
10711
  `;
10371
10712
  if (installMode === "global") {
10372
10713
  files.push({
@@ -10963,6 +11304,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
10963
11304
  projectId: selectedProjectId ?? null
10964
11305
  }, { global: true });
10965
11306
  }
11307
+ await offerCommandScan(dirname4(writtenLocalConfigPath), assumeYes);
10966
11308
  }
10967
11309
  console.log("");
10968
11310
  p4.outro(colors.success("Setup complete!"));
@@ -11010,6 +11352,39 @@ Specify the workspace with --workspace <id>, or select one below.`);
11010
11352
  }
11011
11353
  console.log("");
11012
11354
  }
11355
+ var SCAN_ARGV = ["--yes", "@gethmy/agent@latest", "scan-commands"];
11356
+ function scanTip() {
11357
+ console.log(` ${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.")}`);
11358
+ }
11359
+ async function offerCommandScan(repoDir, assumeYes) {
11360
+ if (assumeYes) {
11361
+ scanTip();
11362
+ return;
11363
+ }
11364
+ const scan = await confirmOrDefault(assumeYes, {
11365
+ message: "Scan this repo's commands now? It runs your build, test and dev scripts once to prove which ones exist. Nothing is written.",
11366
+ initialValue: true
11367
+ });
11368
+ if (p4.isCancel(scan)) {
11369
+ p4.cancel("Setup cancelled.");
11370
+ process.exit(0);
11371
+ }
11372
+ if (!scan) {
11373
+ scanTip();
11374
+ return;
11375
+ }
11376
+ console.log(` ${colors.dim("Running")} ${colors.highlight(`npx ${SCAN_ARGV.slice(1).join(" ")}`)}${colors.dim(" …")}`);
11377
+ const result = spawnSync("npx", [...SCAN_ARGV], {
11378
+ cwd: repoDir,
11379
+ stdio: "inherit"
11380
+ });
11381
+ if (result.error || result.status !== 0) {
11382
+ console.log(` ${colors.dim("The scan did not run — your installed @gethmy/agent may predate it.")}`);
11383
+ scanTip();
11384
+ return;
11385
+ }
11386
+ console.log(` ${colors.dim("Re-run it with")} ${colors.highlight("--write")} ${colors.dim("to merge that block into the pin.")}`);
11387
+ }
11013
11388
 
11014
11389
  // src/cli.ts
11015
11390
  var require2 = createRequire2(import.meta.url);