@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/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)
@@ -31,11 +32,28 @@ function noteLegacyLocalPin(path) {
31
32
  if (warnedLegacyLocalPin)
32
33
  return;
33
34
  warnedLegacyLocalPin = true;
34
- console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Rename it to ${LOCAL_CONFIG_FILENAME} the fallback that finds it is temporary.`);
35
+ console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Run \`harmony-agent doctor --fix\` in this repo to write ` + `${LOCAL_CONFIG_FILENAME}, or rename the file yourself. ` + `The fallback that finds it is temporary.`);
35
36
  }
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"]
@@ -2204,6 +2299,60 @@ var REVIEW_DISALLOWED_TOOLS = [
2204
2299
  "mcp__harmony__harmony_delete_subtask",
2205
2300
  "mcp__harmony__harmony_toggle_subtask"
2206
2301
  ];
2302
+ // ../harmony-shared/dist/runEventSanitize.js
2303
+ var REPLACEMENT = "�";
2304
+ function sanitizeRunEventString(value) {
2305
+ let out = "";
2306
+ for (let i = 0;i < value.length; i++) {
2307
+ const code = value.charCodeAt(i);
2308
+ if (code === 0)
2309
+ continue;
2310
+ if (code >= 55296 && code <= 56319) {
2311
+ const next = value.charCodeAt(i + 1);
2312
+ if (next >= 56320 && next <= 57343) {
2313
+ out += value[i] + value[i + 1];
2314
+ i++;
2315
+ continue;
2316
+ }
2317
+ out += REPLACEMENT;
2318
+ continue;
2319
+ }
2320
+ if (code >= 56320 && code <= 57343) {
2321
+ out += REPLACEMENT;
2322
+ continue;
2323
+ }
2324
+ out += value[i];
2325
+ }
2326
+ return out;
2327
+ }
2328
+ function sanitizeRunEventPayload(payload) {
2329
+ return walk(payload, new Map);
2330
+ }
2331
+ function sanitizeRunEventDraft(draft) {
2332
+ return { ...draft, payload: sanitizeRunEventPayload(draft.payload) };
2333
+ }
2334
+ function walk(value, seen) {
2335
+ if (typeof value === "string")
2336
+ return sanitizeRunEventString(value);
2337
+ if (value === null || typeof value !== "object")
2338
+ return value;
2339
+ const already = seen.get(value);
2340
+ if (already !== undefined)
2341
+ return already;
2342
+ if (Array.isArray(value)) {
2343
+ const out2 = [];
2344
+ seen.set(value, out2);
2345
+ for (const entry of value)
2346
+ out2.push(walk(entry, seen));
2347
+ return out2;
2348
+ }
2349
+ const out = {};
2350
+ seen.set(value, out);
2351
+ for (const [key, entry] of Object.entries(value)) {
2352
+ out[sanitizeRunEventString(key)] = walk(entry, seen);
2353
+ }
2354
+ return out;
2355
+ }
2207
2356
  // ../harmony-shared/dist/runRedaction.js
2208
2357
  var MAX_INPUT_CHARS = 2000;
2209
2358
  var MAX_OUTPUT_CHARS = 4000;
@@ -2724,6 +2873,15 @@ class HarmonyApiClient {
2724
2873
  async registerWorkspaceAgent(workspaceId, data) {
2725
2874
  return this.request("POST", `/workspaces/${workspaceId}/agents`, data);
2726
2875
  }
2876
+ async reportAgentConfig(workspaceId, agentId, config) {
2877
+ return this.request("POST", `/workspaces/${workspaceId}/agents/${agentId}/reported-config`, { config });
2878
+ }
2879
+ async getWorkspaceModelConfig(workspaceId) {
2880
+ return this.request("GET", `/workspaces/${workspaceId}/model-config`);
2881
+ }
2882
+ async getModelCatalog() {
2883
+ return this.request("GET", "/model-catalog");
2884
+ }
2727
2885
  async listProjects(workspaceId) {
2728
2886
  return this.request("GET", `/workspaces/${workspaceId}/projects`);
2729
2887
  }
@@ -2872,6 +3030,9 @@ class HarmonyApiClient {
2872
3030
  title
2873
3031
  });
2874
3032
  }
3033
+ async removeExternalLink(cardId, linkId) {
3034
+ return this.request("DELETE", `/cards/${cardId}/external-links/${linkId}`);
3035
+ }
2875
3036
  async uploadArtifact(data) {
2876
3037
  return this.request("POST", "/artifacts", data);
2877
3038
  }
@@ -2964,7 +3125,10 @@ class HarmonyApiClient {
2964
3125
  return this.request("DELETE", `/cards/${cardId}/agent-context`, data);
2965
3126
  }
2966
3127
  async appendAgentRunEvents(cardId, data) {
2967
- return this.request("POST", `/cards/${cardId}/agent-run-events`, data);
3128
+ return this.request("POST", `/cards/${cardId}/agent-run-events`, {
3129
+ ...data,
3130
+ events: data.events.map((event) => sanitizeRunEventDraft(event))
3131
+ });
2968
3132
  }
2969
3133
  async getPendingUserMessages(cardId, sessionId, sinceSeq) {
2970
3134
  return this.request("GET", `/cards/${cardId}/agent-messages?sessionId=${sessionId}&sinceSeq=${sinceSeq}`);
@@ -4617,81 +4781,6 @@ function parseHmyConfig(text) {
4617
4781
  }
4618
4782
 
4619
4783
  // src/skills.ts
4620
- var HARMONY_WORKFLOW_PROMPT = `# Harmony Card Workflow
4621
-
4622
- Start work on a Harmony card. Card reference: $ARGUMENTS
4623
-
4624
- ## 1. Find & Fetch Card
4625
-
4626
- Parse the reference and fetch the card:
4627
- - \`#42\` or \`42\` → \`harmony_get_card\` with \`shortId: 42\`
4628
- - UUID → \`harmony_get_card\` with \`cardId\`
4629
- - Name/text → \`harmony_search_cards\` with \`query\`
4630
-
4631
- ## 2. Get Board State
4632
-
4633
- Call \`harmony_get_board\` to get columns and labels. From the response:
4634
- - Find the "In Progress" (or "Progress") column ID
4635
- - Find the "agent" label ID
4636
-
4637
- ## 3. Setup Card for Work
4638
-
4639
- Execute these in sequence:
4640
- 1. \`harmony_move_card\` → Move to "In Progress" column
4641
- 2. \`harmony_add_label_to_card\` → Add "agent" label
4642
- 3. \`harmony_start_agent_session\`:
4643
- - \`cardId\`: Card UUID
4644
- - \`agentIdentifier\`: Your agent identifier
4645
- - \`agentName\`: Your agent name
4646
- - \`currentTask\`: "Analyzing card requirements"
4647
-
4648
- ## 4. Generate Work Prompt
4649
-
4650
- Call \`harmony_generate_prompt\` with:
4651
- - \`cardId\` or \`shortId\` (+ \`projectId\` if using shortId)
4652
- - \`variant\`: Select based on task:
4653
- - \`"execute"\` (default) → Clear tasks, bug fixes, well-defined work
4654
- - \`"analysis"\` → Complex features, unclear requirements
4655
- - \`"draft"\` → Medium complexity, want feedback first
4656
-
4657
- The generated prompt provides role framing, focus areas, subtasks, linked cards, and suggested outputs.
4658
-
4659
- ## 5. Display Card Summary
4660
-
4661
- Show the user: Card title, short ID, role, priority, labels, due date, description, and subtasks.
4662
-
4663
- ## 6. Implement Solution
4664
-
4665
- Work on the card following the generated prompt's guidance. Update progress at milestones:
4666
- - \`harmony_update_agent_progress\` with \`progressPercent\` (0-100), \`currentTask\`, \`status\`, \`blockers\`
4667
-
4668
- **Progress checkpoints:** 20% (exploration), 50% (implementation), 80% (testing), 100% (done)
4669
-
4670
- ## 7. Complete Work
4671
-
4672
- When finished:
4673
- 1. \`harmony_end_agent_session\` with \`status: "completed"\`, \`progressPercent: 100\`
4674
- 2. \`harmony_move_card\` to "Review" column
4675
- 3. Summarize accomplishments
4676
-
4677
- If pausing: \`harmony_end_agent_session\` with \`status: "paused"\`
4678
-
4679
- ## Key Tools Reference
4680
-
4681
- **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\`
4682
-
4683
- **Subtasks:** \`harmony_create_subtask\`, \`harmony_toggle_subtask\`, \`harmony_delete_subtask\`
4684
-
4685
- **Labels:** \`harmony_add_label_to_card\`, \`harmony_remove_label_from_card\`, \`harmony_create_label\`
4686
-
4687
- **Links:** \`harmony_add_link_to_card\`, \`harmony_remove_link_from_card\`, \`harmony_get_card_links\`
4688
-
4689
- **Board:** \`harmony_get_board\`, \`harmony_list_projects\`, \`harmony_get_context\`, \`harmony_set_project_context\`
4690
-
4691
- **Sessions:** \`harmony_start_agent_session\`, \`harmony_update_agent_progress\`, \`harmony_end_agent_session\`, \`harmony_get_agent_session\`
4692
-
4693
- **AI:** \`harmony_generate_prompt\`, \`harmony_process_command\`
4694
- `;
4695
4784
  function buildSkillFile(skill) {
4696
4785
  const content = stripSkillPreamble(skill.content);
4697
4786
  if (skill.skillVersion !== undefined && !hasMetadataVersion(content)) {
@@ -5654,6 +5743,23 @@ var TOOLS = {
5654
5743
  required: ["cardId", "url"]
5655
5744
  }
5656
5745
  },
5746
+ harmony_remove_external_link: {
5747
+ description: "Remove an external reference URL from a card — the counterpart to harmony_add_external_link. Takes the link id from harmony_get_card_external_links, not the URL.",
5748
+ inputSchema: {
5749
+ type: "object",
5750
+ properties: {
5751
+ cardId: {
5752
+ type: "string",
5753
+ description: "Card UUID"
5754
+ },
5755
+ linkId: {
5756
+ type: "string",
5757
+ description: "External link UUID, as returned by harmony_get_card_external_links"
5758
+ }
5759
+ },
5760
+ required: ["cardId", "linkId"]
5761
+ }
5762
+ },
5657
5763
  harmony_create_subtask: {
5658
5764
  description: "Create a subtask on a card",
5659
5765
  inputSchema: {
@@ -6529,7 +6635,7 @@ var TOOLS = {
6529
6635
  }
6530
6636
  },
6531
6637
  harmony_create_plan: {
6532
- 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.",
6533
6639
  inputSchema: {
6534
6640
  type: "object",
6535
6641
  properties: {
@@ -6552,7 +6658,10 @@ var TOOLS = {
6552
6658
  items: {
6553
6659
  type: "object",
6554
6660
  properties: {
6555
- 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
+ },
6556
6665
  priority: {
6557
6666
  type: "string",
6558
6667
  enum: ["high", "medium", "low"],
@@ -6566,7 +6675,7 @@ var TOOLS = {
6566
6675
  },
6567
6676
  required: ["content"]
6568
6677
  },
6569
- 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."
6570
6679
  }
6571
6680
  },
6572
6681
  required: ["title"]
@@ -6587,7 +6696,7 @@ var TOOLS = {
6587
6696
  }
6588
6697
  },
6589
6698
  harmony_update_plan: {
6590
- 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.",
6591
6700
  inputSchema: {
6592
6701
  type: "object",
6593
6702
  properties: {
@@ -6601,6 +6710,16 @@ var TOOLS = {
6601
6710
  type: "string",
6602
6711
  enum: ["draft", "active", "archived"],
6603
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."
6604
6723
  }
6605
6724
  },
6606
6725
  required: ["planId"]
@@ -7548,6 +7667,11 @@ ${list}
7548
7667
  const result = await client3.addExternalLink(cardId, url, title);
7549
7668
  return { success: true, ...result };
7550
7669
  }
7670
+ case "harmony_remove_external_link": {
7671
+ const cardId = z.string().uuid().parse(args.cardId);
7672
+ const linkId = z.string().uuid().parse(args.linkId);
7673
+ return await client3.removeExternalLink(cardId, linkId);
7674
+ }
7551
7675
  case "harmony_classify_card":
7552
7676
  return deprecatedRemovedToolResult("harmony_classify_card");
7553
7677
  case "harmony_create_subtask": {
@@ -8580,6 +8704,13 @@ ${options}
8580
8704
  if (args.status !== undefined) {
8581
8705
  updates.status = z.enum(["draft", "active", "archived"]).parse(args.status);
8582
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);
8583
8714
  const result = await client3.updatePlan(planId, updates);
8584
8715
  return { success: true, plan: result.plan };
8585
8716
  }
@@ -8879,6 +9010,7 @@ class HarmonyMCPServer {
8879
9010
  }
8880
9011
 
8881
9012
  // src/tui/setup.ts
9013
+ import { spawnSync } from "node:child_process";
8882
9014
  import { createHash as createHash5 } from "node:crypto";
8883
9015
  import {
8884
9016
  existsSync as existsSync9,
@@ -8893,6 +9025,288 @@ import * as p4 from "@clack/prompts";
8893
9025
  init_config();
8894
9026
  init_oauth_login();
8895
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
+
8896
9310
  // src/tui/agents.ts
8897
9311
  import { existsSync as existsSync6 } from "node:fs";
8898
9312
  import { homedir as homedir5 } from "node:os";
@@ -9792,6 +10206,67 @@ function appendToToml(filePath, section, content, options = {}) {
9792
10206
  };
9793
10207
  }
9794
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
+ }
9795
10270
  async function writeFilesWithProgress(files, options = {}) {
9796
10271
  const results = [];
9797
10272
  const home = homedir6();
@@ -9804,6 +10279,8 @@ async function writeFilesWithProgress(files, options = {}) {
9804
10279
  result = mergeJsonFile(file.path, jsonContent, options);
9805
10280
  } else if (file.type === "toml" && file.tomlSection) {
9806
10281
  result = appendToToml(file.path, file.tomlSection, file.content, options);
10282
+ } else if (file.type === "markdown") {
10283
+ result = mergeMarkdownSection(file.path, file.content, options);
9807
10284
  } else {
9808
10285
  result = writeFile(file.path, file.content, {
9809
10286
  ...options,
@@ -9821,7 +10298,7 @@ async function writeFilesWithProgress(files, options = {}) {
9821
10298
  } else if (result.action === "skip") {
9822
10299
  console.log(messages.fileSkipped(displayPath));
9823
10300
  } else {
9824
- const actionLabel = result.action === "merge" ? "updated" : "created";
10301
+ const actionLabel = result.action === "create" ? "created" : result.action === "merge" ? "merged" : "updated";
9825
10302
  console.log(` ${colors.success("✓")} ${colors.dim(displayPath)} ${colors.dim(`(${actionLabel})`)}`);
9826
10303
  }
9827
10304
  }
@@ -9849,7 +10326,6 @@ function getWriteSummary(files, options = {}) {
9849
10326
  // src/tui/setup.ts
9850
10327
  var SAFE_HARMONY_TOOLS = [
9851
10328
  "harmony_get_card",
9852
- "harmony_get_card_by_short_id",
9853
10329
  "harmony_search_cards",
9854
10330
  "harmony_get_board",
9855
10331
  "harmony_get_context",
@@ -9861,12 +10337,17 @@ var SAFE_HARMONY_TOOLS = [
9861
10337
  "harmony_get_comments",
9862
10338
  "harmony_get_plan",
9863
10339
  "harmony_list_plans",
10340
+ "harmony_get_playbook",
10341
+ "harmony_list_playbook",
9864
10342
  "harmony_get_agent_session",
10343
+ "harmony_get_pending_messages",
9865
10344
  "harmony_get_workspace_members",
9866
10345
  "harmony_list_agents",
9867
10346
  "harmony_resolve_links",
10347
+ "harmony_suggest_relations",
9868
10348
  "harmony_recall",
9869
10349
  "harmony_memory_search",
10350
+ "harmony_vault_index",
9870
10351
  "harmony_generate_prompt",
9871
10352
  "harmony_create_card",
9872
10353
  "harmony_update_card",
@@ -9874,6 +10355,7 @@ var SAFE_HARMONY_TOOLS = [
9874
10355
  "harmony_assign_card",
9875
10356
  "harmony_create_subtask",
9876
10357
  "harmony_toggle_subtask",
10358
+ "harmony_update_subtask",
9877
10359
  "harmony_add_label_to_card",
9878
10360
  "harmony_remove_label_from_card",
9879
10361
  "harmony_create_label",
@@ -9881,6 +10363,8 @@ var SAFE_HARMONY_TOOLS = [
9881
10363
  "harmony_update_comment",
9882
10364
  "harmony_add_link_to_card",
9883
10365
  "harmony_remove_link_from_card",
10366
+ "harmony_add_external_link",
10367
+ "harmony_remove_external_link",
9884
10368
  "harmony_start_agent_session",
9885
10369
  "harmony_update_agent_progress",
9886
10370
  "harmony_end_agent_session",
@@ -9893,6 +10377,7 @@ var SAFE_HARMONY_TOOLS = [
9893
10377
  "harmony_remember",
9894
10378
  "harmony_relate",
9895
10379
  "harmony_update_memory",
10380
+ "harmony_recall_feedback",
9896
10381
  "harmony_process_command",
9897
10382
  "harmony_sync"
9898
10383
  ];
@@ -10106,63 +10591,10 @@ ${summary}`);
10106
10591
  break;
10107
10592
  }
10108
10593
  case "codex": {
10109
- const agentsContent = `# Harmony Integration
10110
-
10111
- This project uses Harmony for task management. When working on tasks:
10112
-
10113
- ## Agent identity — always identify as yourself
10114
-
10115
- Every \`harmony_start_agent_session\` call passes \`agentIdentifier\` + \`agentName\`. **Use your own
10116
- identity, never a hardcoded one from this file.** AGENTS.md is a cross-runtime convention file, so
10117
- more than one kind of agent will read it; the board shows agents as teammates, and a session
10118
- attributed to the wrong runtime misattributes the work in front of the whole team.
10119
-
10120
- - \`agentIdentifier\` — a stable kebab-case id for the runtime you actually are
10121
- - \`agentName\` — its human-readable name
10122
-
10123
- Known values: \`claude-code\` / "Claude Code", \`codex\` / "OpenAI Codex", \`cursor\` / "Cursor",
10124
- \`claude-desktop\` / "Claude Desktop". If you are a runtime not listed here, use your own name rather
10125
- than borrowing the closest entry.
10126
-
10127
- ## Starting Work on a Card
10128
-
10129
- When given a card reference (e.g., #42 or a card name), follow this workflow:
10130
-
10131
- 1. Use \`harmony_get_card\` or \`harmony_search_cards\` to find the card
10132
- 2. Move the card to "In Progress" using \`harmony_move_card\`
10133
- 3. Add the "agent" label using \`harmony_add_label_to_card\`
10134
- 4. Start a session with \`harmony_start_agent_session\`, passing **your own** \`agentIdentifier\` +
10135
- \`agentName\` (see "Agent identity" above)
10136
- 5. Show the card details to the user
10137
- 6. Use \`harmony_generate_prompt\` to get guidance, then implement the solution
10138
- 7. Update progress periodically with \`harmony_update_agent_progress\`
10139
- 8. When done, call \`harmony_end_agent_session\` and move to "Review"
10140
-
10141
- ## Auto-Detect Card for Implementation Tasks
10142
-
10143
- Before implementing a plan or feature, check if it maps to an existing Harmony card:
10144
-
10145
- 1. Use \`harmony_search_cards\` with keywords from the task description
10146
- 2. If a match is found, call \`harmony_start_agent_session\` with **your own** \`agentIdentifier\` +
10147
- \`agentName\` (see "Agent identity" above), plus \`moveToColumn: "In Progress"\`, \`addLabels: ["agent"]\`
10148
- 3. Update progress with \`harmony_update_agent_progress\` at milestones
10149
- 4. When done, call \`harmony_end_agent_session\` with status: "completed", moveToColumn: "Review"
10150
-
10151
- Skip if: work was already started with a card reference, or no matching card exists.
10152
-
10153
- ## Available Harmony Tools
10154
-
10155
- - \`harmony_get_card\`, \`harmony_get_card_by_short_id\`, \`harmony_search_cards\` - Find cards
10156
- - \`harmony_move_card\` - Move cards between columns
10157
- - \`harmony_add_label_to_card\`, \`harmony_remove_label_from_card\` - Manage labels
10158
- - \`harmony_start_agent_session\`, \`harmony_update_agent_progress\`, \`harmony_end_agent_session\` - Track work
10159
- - \`harmony_get_board\` - Get board state
10160
- - \`harmony_generate_prompt\` - Get role-based guidance and focus areas for the card
10161
- `;
10162
10594
  files.push({
10163
10595
  path: join9(cwd, "AGENTS.md"),
10164
- content: agentsContent,
10165
- type: "text"
10596
+ content: HARMONY_AGENTS_SECTION,
10597
+ type: "markdown"
10166
10598
  });
10167
10599
  const promptContent = `---
10168
10600
  name: hmy
@@ -10173,7 +10605,7 @@ arguments:
10173
10605
  required: true
10174
10606
  ---
10175
10607
 
10176
- ${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" })}
10177
10609
  `;
10178
10610
  if (installMode === "global") {
10179
10611
  files.push({
@@ -10230,7 +10662,7 @@ alwaysApply: false
10230
10662
 
10231
10663
  When the user asks you to work on a Harmony card (references like #42, card names, or UUIDs):
10232
10664
 
10233
- ${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" })}
10234
10666
  `;
10235
10667
  if (installMode === "global") {
10236
10668
  files.push({
@@ -10275,7 +10707,7 @@ description: Activate when user asks to work on a Harmony card (references like
10275
10707
 
10276
10708
  When working on a Harmony card:
10277
10709
 
10278
- ${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" })}
10279
10711
  `;
10280
10712
  if (installMode === "global") {
10281
10713
  files.push({
@@ -10872,6 +11304,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
10872
11304
  projectId: selectedProjectId ?? null
10873
11305
  }, { global: true });
10874
11306
  }
11307
+ await offerCommandScan(dirname4(writtenLocalConfigPath), assumeYes);
10875
11308
  }
10876
11309
  console.log("");
10877
11310
  p4.outro(colors.success("Setup complete!"));
@@ -10919,6 +11352,39 @@ Specify the workspace with --workspace <id>, or select one below.`);
10919
11352
  }
10920
11353
  console.log("");
10921
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
+ }
10922
11388
 
10923
11389
  // src/cli.ts
10924
11390
  var require2 = createRequire2(import.meta.url);