@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/README.md +3 -3
- package/dist/cli.js +623 -157
- package/dist/index.js +228 -97
- package/dist/lib/api-client.js +181 -16
- package/dist/lib/config.js +110 -14
- package/dist/lib/oauth-refresh.js +110 -14
- package/dist/run-hook-cli.js +54 -0
- package/package.json +2 -2
- package/src/api-client.ts +91 -1
- package/src/config.ts +262 -14
- package/src/prompt-builder.ts +1 -1
- package/src/server.ts +70 -4
- package/src/skills.ts +6 -80
- package/src/tui/agent-instructions.ts +335 -0
- package/src/tui/setup.ts +144 -63
- package/src/tui/writer.ts +118 -2
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. ` + `
|
|
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
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
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:
|
|
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 =
|
|
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`,
|
|
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
|
|
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: {
|
|
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: "
|
|
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
|
|
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" ? "
|
|
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:
|
|
10165
|
-
type: "
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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);
|