@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/index.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 as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
22
- import { homedir } from "node:os";
23
+ import { homedir, tmpdir } from "node:os";
23
24
  import { dirname, join as join2, 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 join2(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
- writeFileSync2(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
+ writeFileSync2(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 = readFileSync2(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 = join2(tmpdir(), `hmy-pin-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json.bak`);
211
+ writeFileSync2(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(join2(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(join2(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 readFileSync2(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 join2(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"]
@@ -4469,81 +4564,6 @@ function parseHmyConfig(text) {
4469
4564
  }
4470
4565
 
4471
4566
  // src/skills.ts
4472
- var HARMONY_WORKFLOW_PROMPT = `# Harmony Card Workflow
4473
-
4474
- Start work on a Harmony card. Card reference: $ARGUMENTS
4475
-
4476
- ## 1. Find & Fetch Card
4477
-
4478
- Parse the reference and fetch the card:
4479
- - \`#42\` or \`42\` → \`harmony_get_card\` with \`shortId: 42\`
4480
- - UUID → \`harmony_get_card\` with \`cardId\`
4481
- - Name/text → \`harmony_search_cards\` with \`query\`
4482
-
4483
- ## 2. Get Board State
4484
-
4485
- Call \`harmony_get_board\` to get columns and labels. From the response:
4486
- - Find the "In Progress" (or "Progress") column ID
4487
- - Find the "agent" label ID
4488
-
4489
- ## 3. Setup Card for Work
4490
-
4491
- Execute these in sequence:
4492
- 1. \`harmony_move_card\` → Move to "In Progress" column
4493
- 2. \`harmony_add_label_to_card\` → Add "agent" label
4494
- 3. \`harmony_start_agent_session\`:
4495
- - \`cardId\`: Card UUID
4496
- - \`agentIdentifier\`: Your agent identifier
4497
- - \`agentName\`: Your agent name
4498
- - \`currentTask\`: "Analyzing card requirements"
4499
-
4500
- ## 4. Generate Work Prompt
4501
-
4502
- Call \`harmony_generate_prompt\` with:
4503
- - \`cardId\` or \`shortId\` (+ \`projectId\` if using shortId)
4504
- - \`variant\`: Select based on task:
4505
- - \`"execute"\` (default) → Clear tasks, bug fixes, well-defined work
4506
- - \`"analysis"\` → Complex features, unclear requirements
4507
- - \`"draft"\` → Medium complexity, want feedback first
4508
-
4509
- The generated prompt provides role framing, focus areas, subtasks, linked cards, and suggested outputs.
4510
-
4511
- ## 5. Display Card Summary
4512
-
4513
- Show the user: Card title, short ID, role, priority, labels, due date, description, and subtasks.
4514
-
4515
- ## 6. Implement Solution
4516
-
4517
- Work on the card following the generated prompt's guidance. Update progress at milestones:
4518
- - \`harmony_update_agent_progress\` with \`progressPercent\` (0-100), \`currentTask\`, \`status\`, \`blockers\`
4519
-
4520
- **Progress checkpoints:** 20% (exploration), 50% (implementation), 80% (testing), 100% (done)
4521
-
4522
- ## 7. Complete Work
4523
-
4524
- When finished:
4525
- 1. \`harmony_end_agent_session\` with \`status: "completed"\`, \`progressPercent: 100\`
4526
- 2. \`harmony_move_card\` to "Review" column
4527
- 3. Summarize accomplishments
4528
-
4529
- If pausing: \`harmony_end_agent_session\` with \`status: "paused"\`
4530
-
4531
- ## Key Tools Reference
4532
-
4533
- **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\`
4534
-
4535
- **Subtasks:** \`harmony_create_subtask\`, \`harmony_toggle_subtask\`, \`harmony_delete_subtask\`
4536
-
4537
- **Labels:** \`harmony_add_label_to_card\`, \`harmony_remove_label_from_card\`, \`harmony_create_label\`
4538
-
4539
- **Links:** \`harmony_add_link_to_card\`, \`harmony_remove_link_from_card\`, \`harmony_get_card_links\`
4540
-
4541
- **Board:** \`harmony_get_board\`, \`harmony_list_projects\`, \`harmony_get_context\`, \`harmony_set_project_context\`
4542
-
4543
- **Sessions:** \`harmony_start_agent_session\`, \`harmony_update_agent_progress\`, \`harmony_end_agent_session\`, \`harmony_get_agent_session\`
4544
-
4545
- **AI:** \`harmony_generate_prompt\`, \`harmony_process_command\`
4546
- `;
4547
4567
  function buildSkillFile(skill) {
4548
4568
  const content = stripSkillPreamble(skill.content);
4549
4569
  if (skill.skillVersion !== undefined && !hasMetadataVersion(content)) {
@@ -6398,7 +6418,7 @@ var TOOLS = {
6398
6418
  }
6399
6419
  },
6400
6420
  harmony_create_plan: {
6401
- 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.",
6421
+ 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.",
6402
6422
  inputSchema: {
6403
6423
  type: "object",
6404
6424
  properties: {
@@ -6421,7 +6441,10 @@ var TOOLS = {
6421
6441
  items: {
6422
6442
  type: "object",
6423
6443
  properties: {
6424
- content: { type: "string", description: "Task description" },
6444
+ content: {
6445
+ type: "string",
6446
+ 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.'
6447
+ },
6425
6448
  priority: {
6426
6449
  type: "string",
6427
6450
  enum: ["high", "medium", "low"],
@@ -6435,7 +6458,7 @@ var TOOLS = {
6435
6458
  },
6436
6459
  required: ["content"]
6437
6460
  },
6438
- description: "Optional list of tasks to create with the plan"
6461
+ 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."
6439
6462
  }
6440
6463
  },
6441
6464
  required: ["title"]
@@ -6456,7 +6479,7 @@ var TOOLS = {
6456
6479
  }
6457
6480
  },
6458
6481
  harmony_update_plan: {
6459
- description: "Update an existing plan. Can update title, content, or status.",
6482
+ 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.",
6460
6483
  inputSchema: {
6461
6484
  type: "object",
6462
6485
  properties: {
@@ -6470,6 +6493,16 @@ var TOOLS = {
6470
6493
  type: "string",
6471
6494
  enum: ["draft", "active", "archived"],
6472
6495
  description: "New status"
6496
+ },
6497
+ startDate: {
6498
+ type: "string",
6499
+ nullable: true,
6500
+ description: "Timeline start as YYYY-MM-DD, or null to unpin (send endDate null too)."
6501
+ },
6502
+ endDate: {
6503
+ type: "string",
6504
+ nullable: true,
6505
+ description: "Timeline end as YYYY-MM-DD, or null to unpin (send startDate null too). Must not precede startDate."
6473
6506
  }
6474
6507
  },
6475
6508
  required: ["planId"]
@@ -8454,6 +8487,13 @@ ${options}
8454
8487
  if (args.status !== undefined) {
8455
8488
  updates.status = z.enum(["draft", "active", "archived"]).parse(args.status);
8456
8489
  }
8490
+ const planDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
8491
+ message: "expected a date as YYYY-MM-DD"
8492
+ });
8493
+ if (args.startDate !== undefined)
8494
+ updates.startDate = planDate.nullable().parse(args.startDate);
8495
+ if (args.endDate !== undefined)
8496
+ updates.endDate = planDate.nullable().parse(args.endDate);
8457
8497
  const result = await client3.updatePlan(planId, updates);
8458
8498
  return { success: true, plan: result.plan };
8459
8499
  }
@@ -15,12 +15,14 @@ var __export = (target, all) => {
15
15
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
16
16
 
17
17
  // src/config.ts
18
+ import { execFileSync } from "node:child_process";
18
19
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
- import { homedir } from "node:os";
20
+ import { homedir, tmpdir } from "node:os";
20
21
  import { dirname, join, parse, resolve } from "node:path";
21
22
  function resetLegacyNoticesForTest() {
22
23
  warnedLegacyConfigDir = false;
23
24
  warnedLegacyLocalPin = false;
25
+ warnedUntrackedLocalPin = false;
24
26
  }
25
27
  function noteLegacyConfigDir(path) {
26
28
  if (warnedLegacyConfigDir)
@@ -37,6 +39,23 @@ function noteLegacyLocalPin(path) {
37
39
  function noteLocalPinRename(from, to) {
38
40
  console.error(`Harmony: wrote the repo pin to ${to} (the pre-#1082 ${from} is now ignored and can be deleted).`);
39
41
  }
42
+ function noteUntrackedLocalPin(path) {
43
+ if (warnedUntrackedLocalPin)
44
+ return;
45
+ warnedUntrackedLocalPin = true;
46
+ 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.`);
47
+ }
48
+ function isGitIgnored(path) {
49
+ try {
50
+ execFileSync("git", ["check-ignore", "--quiet", path], {
51
+ cwd: dirname(path),
52
+ stdio: "ignore"
53
+ });
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
40
59
  function getHmyRootDir() {
41
60
  return join(homedir(), CONFIG_DIR_NAME);
42
61
  }
@@ -151,19 +170,96 @@ function saveLocalConfig(config, cwd) {
151
170
  if (foundPath !== null && foundPath !== localConfigPath) {
152
171
  noteLocalPinRename(foundPath, localConfigPath);
153
172
  }
154
- const existingConfig = loadLocalConfig(cwd) || {
155
- workspaceId: null,
156
- projectId: null
157
- };
158
- const newConfig = { ...existingConfig, ...config };
159
- const cleanConfig = {};
160
- if (newConfig.workspaceId)
161
- cleanConfig.workspaceId = newConfig.workspaceId;
162
- if (newConfig.projectId)
163
- cleanConfig.projectId = newConfig.projectId;
164
- writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
173
+ const existing = readRawLocalConfig(foundPath ?? localConfigPath);
174
+ const merged = { ...existing };
175
+ if ("workspaceId" in config) {
176
+ if (config.workspaceId)
177
+ merged.workspaceId = config.workspaceId;
178
+ else
179
+ delete merged.workspaceId;
180
+ }
181
+ if ("projectId" in config) {
182
+ if (config.projectId)
183
+ merged.projectId = config.projectId;
184
+ else
185
+ delete merged.projectId;
186
+ }
187
+ writeFileSync(localConfigPath, `${JSON.stringify(merged, null, localIndent(localConfigPath))}
188
+ `);
189
+ if (isGitIgnored(localConfigPath))
190
+ noteUntrackedLocalPin(localConfigPath);
165
191
  return localConfigPath;
166
192
  }
193
+ function readRawLocalConfig(path) {
194
+ let text;
195
+ try {
196
+ text = readFileSync(path, "utf-8");
197
+ } catch {
198
+ return {};
199
+ }
200
+ try {
201
+ const parsed = JSON.parse(text);
202
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
203
+ return parsed;
204
+ }
205
+ } catch {}
206
+ noteUnparsableLocalPin(path, text);
207
+ return {};
208
+ }
209
+ function noteUnparsableLocalPin(path, contents) {
210
+ let backup = null;
211
+ try {
212
+ backup = join(tmpdir(), `hmy-pin-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json.bak`);
213
+ writeFileSync(backup, contents);
214
+ } catch {
215
+ backup = null;
216
+ }
217
+ 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.`);
218
+ }
219
+ function localIndent(configPath) {
220
+ const root = dirname(configPath);
221
+ for (const name of ["biome.json", "biome.jsonc"]) {
222
+ const parsed = readJsonish(join(root, name));
223
+ if (!parsed || typeof parsed !== "object")
224
+ continue;
225
+ const formatter = parsed.formatter;
226
+ if (formatter?.indentStyle === "space") {
227
+ const width = formatter.indentWidth;
228
+ return typeof width === "number" && width > 0 ? width : 2;
229
+ }
230
+ return "\t";
231
+ }
232
+ const editorconfig = readTextSafely(join(root, ".editorconfig"));
233
+ if (editorconfig) {
234
+ if (/^\s*indent_style\s*=\s*tab\s*$/im.test(editorconfig))
235
+ return "\t";
236
+ const size = editorconfig.match(/^\s*indent_size\s*=\s*(\d+)\s*$/im);
237
+ if (size) {
238
+ const width = Number(size[1]);
239
+ if (width > 0)
240
+ return width;
241
+ }
242
+ }
243
+ return 2;
244
+ }
245
+ function readJsonish(path) {
246
+ const text = readTextSafely(path);
247
+ if (text === null)
248
+ return null;
249
+ try {
250
+ const stripped = text.replace(/^\s*\/\/.*$/gm, "").replace(/,(\s*[}\]])/g, "$1");
251
+ return JSON.parse(stripped);
252
+ } catch {
253
+ return null;
254
+ }
255
+ }
256
+ function readTextSafely(path) {
257
+ try {
258
+ return readFileSync(path, "utf-8");
259
+ } catch {
260
+ return null;
261
+ }
262
+ }
167
263
  function hasLocalConfig(cwd) {
168
264
  return findLocalConfigPath(cwd) !== null;
169
265
  }
@@ -300,7 +396,7 @@ function getMemoryDir() {
300
396
  return config.memoryDir;
301
397
  return join(homedir(), ".harmony", "memory");
302
398
  }
303
- 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;
399
+ 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;
304
400
  var init_config = () => {};
305
401
 
306
402
  // src/oauth-login.ts
@@ -864,7 +960,7 @@ var init_prompt_builder = __esm(() => {
864
960
  };
865
961
  VARIANT_INSTRUCTIONS = {
866
962
  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.`,
867
- 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.`,
963
+ 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.`,
868
964
  execute: `EXECUTE MODE: Implement this task completely. Write production-ready code following best practices. Include necessary tests and documentation.`
869
965
  };
870
966
  });
@@ -15,12 +15,14 @@ var __export = (target, all) => {
15
15
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
16
16
 
17
17
  // src/config.ts
18
+ import { execFileSync } from "node:child_process";
18
19
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
- import { homedir } from "node:os";
20
+ import { homedir, tmpdir } from "node:os";
20
21
  import { dirname, join, parse, resolve } from "node:path";
21
22
  function resetLegacyNoticesForTest() {
22
23
  warnedLegacyConfigDir = false;
23
24
  warnedLegacyLocalPin = false;
25
+ warnedUntrackedLocalPin = false;
24
26
  }
25
27
  function noteLegacyConfigDir(path) {
26
28
  if (warnedLegacyConfigDir)
@@ -37,6 +39,23 @@ function noteLegacyLocalPin(path) {
37
39
  function noteLocalPinRename(from, to) {
38
40
  console.error(`Harmony: wrote the repo pin to ${to} (the pre-#1082 ${from} is now ignored and can be deleted).`);
39
41
  }
42
+ function noteUntrackedLocalPin(path) {
43
+ if (warnedUntrackedLocalPin)
44
+ return;
45
+ warnedUntrackedLocalPin = true;
46
+ 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.`);
47
+ }
48
+ function isGitIgnored(path) {
49
+ try {
50
+ execFileSync("git", ["check-ignore", "--quiet", path], {
51
+ cwd: dirname(path),
52
+ stdio: "ignore"
53
+ });
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
40
59
  function getHmyRootDir() {
41
60
  return join(homedir(), CONFIG_DIR_NAME);
42
61
  }
@@ -151,19 +170,96 @@ function saveLocalConfig(config, cwd) {
151
170
  if (foundPath !== null && foundPath !== localConfigPath) {
152
171
  noteLocalPinRename(foundPath, localConfigPath);
153
172
  }
154
- const existingConfig = loadLocalConfig(cwd) || {
155
- workspaceId: null,
156
- projectId: null
157
- };
158
- const newConfig = { ...existingConfig, ...config };
159
- const cleanConfig = {};
160
- if (newConfig.workspaceId)
161
- cleanConfig.workspaceId = newConfig.workspaceId;
162
- if (newConfig.projectId)
163
- cleanConfig.projectId = newConfig.projectId;
164
- writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
173
+ const existing = readRawLocalConfig(foundPath ?? localConfigPath);
174
+ const merged = { ...existing };
175
+ if ("workspaceId" in config) {
176
+ if (config.workspaceId)
177
+ merged.workspaceId = config.workspaceId;
178
+ else
179
+ delete merged.workspaceId;
180
+ }
181
+ if ("projectId" in config) {
182
+ if (config.projectId)
183
+ merged.projectId = config.projectId;
184
+ else
185
+ delete merged.projectId;
186
+ }
187
+ writeFileSync(localConfigPath, `${JSON.stringify(merged, null, localIndent(localConfigPath))}
188
+ `);
189
+ if (isGitIgnored(localConfigPath))
190
+ noteUntrackedLocalPin(localConfigPath);
165
191
  return localConfigPath;
166
192
  }
193
+ function readRawLocalConfig(path) {
194
+ let text;
195
+ try {
196
+ text = readFileSync(path, "utf-8");
197
+ } catch {
198
+ return {};
199
+ }
200
+ try {
201
+ const parsed = JSON.parse(text);
202
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
203
+ return parsed;
204
+ }
205
+ } catch {}
206
+ noteUnparsableLocalPin(path, text);
207
+ return {};
208
+ }
209
+ function noteUnparsableLocalPin(path, contents) {
210
+ let backup = null;
211
+ try {
212
+ backup = join(tmpdir(), `hmy-pin-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json.bak`);
213
+ writeFileSync(backup, contents);
214
+ } catch {
215
+ backup = null;
216
+ }
217
+ 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.`);
218
+ }
219
+ function localIndent(configPath) {
220
+ const root = dirname(configPath);
221
+ for (const name of ["biome.json", "biome.jsonc"]) {
222
+ const parsed = readJsonish(join(root, name));
223
+ if (!parsed || typeof parsed !== "object")
224
+ continue;
225
+ const formatter = parsed.formatter;
226
+ if (formatter?.indentStyle === "space") {
227
+ const width = formatter.indentWidth;
228
+ return typeof width === "number" && width > 0 ? width : 2;
229
+ }
230
+ return "\t";
231
+ }
232
+ const editorconfig = readTextSafely(join(root, ".editorconfig"));
233
+ if (editorconfig) {
234
+ if (/^\s*indent_style\s*=\s*tab\s*$/im.test(editorconfig))
235
+ return "\t";
236
+ const size = editorconfig.match(/^\s*indent_size\s*=\s*(\d+)\s*$/im);
237
+ if (size) {
238
+ const width = Number(size[1]);
239
+ if (width > 0)
240
+ return width;
241
+ }
242
+ }
243
+ return 2;
244
+ }
245
+ function readJsonish(path) {
246
+ const text = readTextSafely(path);
247
+ if (text === null)
248
+ return null;
249
+ try {
250
+ const stripped = text.replace(/^\s*\/\/.*$/gm, "").replace(/,(\s*[}\]])/g, "$1");
251
+ return JSON.parse(stripped);
252
+ } catch {
253
+ return null;
254
+ }
255
+ }
256
+ function readTextSafely(path) {
257
+ try {
258
+ return readFileSync(path, "utf-8");
259
+ } catch {
260
+ return null;
261
+ }
262
+ }
167
263
  function hasLocalConfig(cwd) {
168
264
  return findLocalConfigPath(cwd) !== null;
169
265
  }
@@ -300,7 +396,7 @@ function getMemoryDir() {
300
396
  return config.memoryDir;
301
397
  return join(homedir(), ".harmony", "memory");
302
398
  }
303
- 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;
399
+ 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;
304
400
  var init_config = () => {};
305
401
  init_config();
306
402