@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/README.md +1 -1
- package/dist/cli.js +530 -155
- package/dist/index.js +135 -95
- package/dist/lib/api-client.js +110 -14
- package/dist/lib/config.js +109 -13
- package/dist/lib/oauth-refresh.js +109 -13
- package/package.json +1 -1
- package/src/api-client.ts +9 -0
- package/src/config.ts +243 -12
- package/src/prompt-builder.ts +1 -1
- package/src/server.ts +43 -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
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The instruction text `hmy setup` installs into a project.
|
|
3
|
+
*
|
|
4
|
+
* Two strings, one job: tell an agent how to work a Harmony card correctly.
|
|
5
|
+
* They live here rather than inline in `setup.ts` so a test can read them —
|
|
6
|
+
* `setup-agent-files.test.ts` asserts every `harmony_*` name they mention is
|
|
7
|
+
* advertised by the server's `TOOLS` object. Nothing checked that before
|
|
8
|
+
* #1124, which is how `harmony_get_card_by_short_id` stayed in the installed
|
|
9
|
+
* `AGENTS.md` for four minor versions after 2.14.0 removed it.
|
|
10
|
+
*
|
|
11
|
+
* Both strings are governed by one rule: say only what the tool schemas do not
|
|
12
|
+
* already say. The schemas are in the agent's own tool listing, with live
|
|
13
|
+
* descriptions; restating them here creates a second source of truth that
|
|
14
|
+
* drifts, and that drift is this file's entire history.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The plan rule, in one wording for every runtime.
|
|
19
|
+
*
|
|
20
|
+
* A plan that prescribes the code is transcribed faithfully, defects included —
|
|
21
|
+
* so the rule has to reach the project, not only this repository. It ships three
|
|
22
|
+
* ways: this constant (Codex's `AGENTS.md` section and the Cursor / Windsurf
|
|
23
|
+
* rule files), and the `hmy-plan` skill body for Claude Code. It lives in one
|
|
24
|
+
* constant because a rule that is worded differently in three places is three
|
|
25
|
+
* rules, and only the differences get read.
|
|
26
|
+
*
|
|
27
|
+
* No heading of its own — each caller supplies the level its host file needs.
|
|
28
|
+
*/
|
|
29
|
+
export const HARMONY_PLAN_RULE = `**A plan says what must be true and why. It does not contain the code.** In it: the architecture
|
|
30
|
+
and technology decisions with their reasons, the data model and the API contracts, a short
|
|
31
|
+
signature or schema sketch wherever an interpretation gap would otherwise remain, and success
|
|
32
|
+
criteria a test can check. Not in it: function bodies, control flow, error handling, test code.
|
|
33
|
+
Detail follows risk — a throwaway script gets a rough plan, while auth, money, migrations and
|
|
34
|
+
anything security-relevant get their contracts and edge cases written out.
|
|
35
|
+
|
|
36
|
+
Code in a plan carries the authority of a plan and the quality of a draft that no compiler, test
|
|
37
|
+
or review has read, and an implementer transcribes it faithfully, defects included. So when the
|
|
38
|
+
plan and the code disagree, the code is the evidence: check it, record the decision as a comment,
|
|
39
|
+
and correct the plan as well as the code.`;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The Harmony section of a project's `AGENTS.md`. Written between the
|
|
43
|
+
* `<!-- harmony:start -->` / `<!-- harmony:end -->` markers by
|
|
44
|
+
* `mergeMarkdownSection`, so the rest of the file stays the project's.
|
|
45
|
+
*/
|
|
46
|
+
export const HARMONY_AGENTS_SECTION = `## Harmony
|
|
47
|
+
|
|
48
|
+
This project uses Harmony for task management. The \`harmony_*\` MCP tools are in your tool
|
|
49
|
+
listing with live schemas — read them there. This section covers only what the schemas do not say.
|
|
50
|
+
|
|
51
|
+
### Identify as yourself
|
|
52
|
+
|
|
53
|
+
Every session call takes \`agentIdentifier\` + \`agentName\`. Use your OWN, never a value copied
|
|
54
|
+
from this file. The board shows agents as teammates, so a session attributed to the wrong runtime
|
|
55
|
+
misattributes the work in front of the whole team.
|
|
56
|
+
|
|
57
|
+
Known values: \`claude-code\` / "Claude Code" · \`codex\` / "OpenAI Codex" · \`cursor\` / "Cursor" ·
|
|
58
|
+
\`claude-desktop\` / "Claude Desktop". If you are none of these, use your own name.
|
|
59
|
+
|
|
60
|
+
### Starting work — one call, not three
|
|
61
|
+
|
|
62
|
+
\`harmony_start_agent_session\` moves the card and adds the labels itself. Do not call
|
|
63
|
+
\`harmony_move_card\` or \`harmony_add_label_to_card\` first, and do not fetch the board for a
|
|
64
|
+
column id or a label id — both arguments match by name.
|
|
65
|
+
|
|
66
|
+
\`\`\`
|
|
67
|
+
harmony_start_agent_session({
|
|
68
|
+
cardId,
|
|
69
|
+
agentIdentifier, agentName, // your own
|
|
70
|
+
currentTask: "Reading the auth middleware to find the affected routes",
|
|
71
|
+
moveToColumn: "In Progress",
|
|
72
|
+
addLabels: ["agent"],
|
|
73
|
+
steerable: true, // only if you will poll for steering — see below
|
|
74
|
+
})
|
|
75
|
+
\`\`\`
|
|
76
|
+
|
|
77
|
+
**Then read the reply, because the setup half fails quietly.** \`movedTo\` names the column it
|
|
78
|
+
actually moved to and \`labelsAdded\` the labels it actually added; a miss leaves them null or
|
|
79
|
+
empty and raises no error. The column match is a case-insensitive **substring**, so a board with
|
|
80
|
+
"Ready for Review" ahead of "Review" can take the wrong one. If \`movedTo\` is null or not the
|
|
81
|
+
column you meant, call \`harmony_move_card\` — it matches exactly first and fails loudly, listing
|
|
82
|
+
the columns.
|
|
83
|
+
|
|
84
|
+
Keep the returned \`session.id\`; the steering poll needs it. Then call
|
|
85
|
+
\`harmony_generate_prompt\` for role framing and focus areas — \`variant\` is \`execute\`
|
|
86
|
+
(default), \`analysis\`, or \`draft\`.
|
|
87
|
+
|
|
88
|
+
### Progress — \`actions\` is what survives as evidence
|
|
89
|
+
|
|
90
|
+
On the card itself, \`progressPercent\` and \`currentTask\` each overwrite one field, so the live
|
|
91
|
+
status shows only your latest checkpoint. The timeline keeps more: a checkpoint that carries both
|
|
92
|
+
a \`progressPercent\` and a \`currentTask\` different from the last one leaves a row saying what you
|
|
93
|
+
were **about to do**, and **each entry in \`actions\` leaves a row saying what you actually did**.
|
|
94
|
+
Report four checkpoints with no \`actions\` and a two-hour run reads as four intentions and no
|
|
95
|
+
evidence.
|
|
96
|
+
|
|
97
|
+
Name what you DID since the last checkpoint: the file you edited and why, the gate you ran and
|
|
98
|
+
what it said, the approach you ruled out and on what evidence.
|
|
99
|
+
|
|
100
|
+
\`\`\`
|
|
101
|
+
harmony_update_agent_progress({
|
|
102
|
+
cardId, agentIdentifier, agentName,
|
|
103
|
+
progressPercent: 50,
|
|
104
|
+
currentTask: "Extracting refreshIfExpired() in auth.ts",
|
|
105
|
+
actions: [
|
|
106
|
+
{ description: "Read auth.ts and middleware/session.ts — the refresh path is duplicated in both, which is the actual bug" },
|
|
107
|
+
{ description: "Ruled out patching verifyToken(): three routes depend on its current behaviour" },
|
|
108
|
+
{ description: "Ran bun run lint — green, exit 0" },
|
|
109
|
+
],
|
|
110
|
+
})
|
|
111
|
+
\`\`\`
|
|
112
|
+
|
|
113
|
+
Three to six entries per checkpoint, one sentence each; past 512 characters an entry is silently truncated. Facts, not
|
|
114
|
+
intentions — one vague entry is worse than none. Checkpoints: 20% explored · 50% implementing ·
|
|
115
|
+
80% verifying · 100% done. \`currentTask\` is what you are doing now — never leave it generic.
|
|
116
|
+
|
|
117
|
+
### Steering and Stop
|
|
118
|
+
|
|
119
|
+
If you passed \`steerable: true\`, poll right after every progress update:
|
|
120
|
+
|
|
121
|
+
\`\`\`
|
|
122
|
+
harmony_get_pending_messages({ cardId, sessionId, sinceSeq }) // sinceSeq starts at 0
|
|
123
|
+
\`\`\`
|
|
124
|
+
|
|
125
|
+
Messages come back oldest first. Fold them into the next step and advance \`sinceSeq\` to the
|
|
126
|
+
largest \`seq\` returned, so each is handled exactly once.
|
|
127
|
+
|
|
128
|
+
Two flags come back and mean opposite things:
|
|
129
|
+
|
|
130
|
+
| flag | meaning | what to do |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| \`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. |
|
|
133
|
+
| \`sessionStale: true\` | your session id is no longer live — usually the inactivity sweep | **Nobody stopped you.** Carry on — with a new id. |
|
|
134
|
+
|
|
135
|
+
\`harmony_update_agent_progress\` reports the same two flags, and the recovery from a stale session
|
|
136
|
+
differs by which call told you:
|
|
137
|
+
|
|
138
|
+
- From the **progress** call, a replacement session has already been opened for you and inherited
|
|
139
|
+
the steering channel. Take the new \`session.id\` from that reply and keep going.
|
|
140
|
+
- From the **poll**, nothing was opened. Call \`harmony_start_agent_session\` yourself and poll
|
|
141
|
+
with the id it returns.
|
|
142
|
+
|
|
143
|
+
If the two flags ever disagree, the stop wins.
|
|
144
|
+
|
|
145
|
+
### Finishing
|
|
146
|
+
|
|
147
|
+
\`\`\`
|
|
148
|
+
harmony_end_agent_session({ cardId, status: "completed", progressPercent: 100, moveToColumn: "Review" })
|
|
149
|
+
\`\`\`
|
|
150
|
+
|
|
151
|
+
One call: it moves the card, and on \`status: "completed"\` it also removes the \`agent\` label. Use
|
|
152
|
+
\`status: "paused"\` when you stop mid-flight — that leaves the label on, which is what you want.
|
|
153
|
+
|
|
154
|
+
Attach a PR **after** the session end has moved the card, both ways: \`harmony_add_external_link\`
|
|
155
|
+
(durable — it survives a later description edit) and a \`PR: <url>\` line in the description.
|
|
156
|
+
|
|
157
|
+
### Writing a plan
|
|
158
|
+
|
|
159
|
+
${HARMONY_PLAN_RULE}
|
|
160
|
+
|
|
161
|
+
### Traps
|
|
162
|
+
|
|
163
|
+
- **A \`shortId\` is project-scoped, and the two ways it can miss are opposites.** With **no**
|
|
164
|
+
active project the number is resolved across every project you can reach, so it may come back
|
|
165
|
+
\`needsDisambiguation\` with \`candidates\` — ask which one is meant. With an active project the
|
|
166
|
+
number resolves only inside it, and a card that lives elsewhere fails with an error naming the
|
|
167
|
+
projects it is really in — switch with \`harmony_set_project_context\` or pass \`projectId\`.
|
|
168
|
+
Either way, check \`resolvedProject\` matches the card you meant before starting work.
|
|
169
|
+
- **Fetch many cards in one call:** \`harmony_get_card({ shortIds: [400, 401, 402] })\`, max 100.
|
|
170
|
+
- **Moving a card to a terminal column ends your session** — a column that marks cards done, or
|
|
171
|
+
one named \`done\`, \`completed\` or \`review\`. The response says \`sessionEnded\`.
|
|
172
|
+
- **Read \`harmony_get_comments\` before you act.** Steering messages do not include comments, and
|
|
173
|
+
a later comment outranks an earlier one it contradicts.
|
|
174
|
+
- **Report findings and decisions as comments, not description edits.** \`harmony_add_comment\`
|
|
175
|
+
takes a \`commentType\`: \`question\` and \`blocker\` signal that you need a human; \`decision\`,
|
|
176
|
+
\`finding\`, \`summary\`, \`progress\` and \`message\` are the rest.`;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The per-runtime workflow prompt installed as the Codex prompt file and the
|
|
180
|
+
* Cursor / Windsurf rule files. Rendered through `renderWorkflowPrompt`.
|
|
181
|
+
*/
|
|
182
|
+
export const HARMONY_WORKFLOW_PROMPT = `# Harmony Card Workflow
|
|
183
|
+
|
|
184
|
+
Work a Harmony card. Card reference: $ARGUMENTS
|
|
185
|
+
|
|
186
|
+
The \`harmony_*\` MCP tools are in your tool listing with live schemas — read them there. This
|
|
187
|
+
prompt covers only what the schemas do not say.
|
|
188
|
+
|
|
189
|
+
## 1. Fetch the card
|
|
190
|
+
|
|
191
|
+
- \`#42\` or \`42\` → \`harmony_get_card({ shortId: 42 })\`
|
|
192
|
+
- UUID → \`harmony_get_card({ cardId })\`
|
|
193
|
+
- A name or phrase → \`harmony_search_cards({ query })\`
|
|
194
|
+
- Several at once → \`harmony_get_card({ shortIds: [40, 41, 42] })\`, max 100
|
|
195
|
+
|
|
196
|
+
A \`shortId\` is project-scoped, and the two ways it can miss are opposites. With **no** active
|
|
197
|
+
project the number is resolved across every project you can reach, so it may come back
|
|
198
|
+
\`needsDisambiguation\` with \`candidates\` — ask which one is meant. With an active project the
|
|
199
|
+
number resolves only inside it, and a card that lives elsewhere fails with an error naming the
|
|
200
|
+
projects it is really in; switch with \`harmony_set_project_context\` or pass \`projectId\`.
|
|
201
|
+
Either way, check \`resolvedProject\` before you start.
|
|
202
|
+
|
|
203
|
+
Read \`harmony_get_comments\` too: a later comment outranks an earlier one it contradicts.
|
|
204
|
+
|
|
205
|
+
## 2. Start the session — one call, not three
|
|
206
|
+
|
|
207
|
+
\`harmony_start_agent_session\` moves the card and adds the labels itself. Do not call
|
|
208
|
+
\`harmony_move_card\` or \`harmony_add_label_to_card\` first, and do not fetch the board for a
|
|
209
|
+
column id or a label id — both arguments match by name.
|
|
210
|
+
|
|
211
|
+
\`\`\`
|
|
212
|
+
harmony_start_agent_session({
|
|
213
|
+
cardId,
|
|
214
|
+
agentIdentifier: "$AGENT_IDENTIFIER",
|
|
215
|
+
agentName: "$AGENT_NAME",
|
|
216
|
+
currentTask: "Reading the auth middleware to find the affected routes",
|
|
217
|
+
moveToColumn: "In Progress",
|
|
218
|
+
addLabels: ["agent"],
|
|
219
|
+
steerable: true,
|
|
220
|
+
})
|
|
221
|
+
\`\`\`
|
|
222
|
+
|
|
223
|
+
\`currentTask\` says what you are about to do, specifically. Never "Analyzing card requirements".
|
|
224
|
+
Keep the returned \`session.id\` — step 4 needs it.
|
|
225
|
+
|
|
226
|
+
**Read the reply, because the setup half fails quietly.** \`movedTo\` names the column it actually
|
|
227
|
+
moved to and \`labelsAdded\` the labels it actually added; a miss leaves them null or empty and
|
|
228
|
+
raises no error. The column match is a case-insensitive **substring**, so a board with "Ready for
|
|
229
|
+
Review" ahead of "Review" can take the wrong one. If \`movedTo\` is null or not the column you
|
|
230
|
+
meant, call \`harmony_move_card\` — it matches exactly first and fails loudly, listing the columns.
|
|
231
|
+
|
|
232
|
+
## 3. Get the work prompt
|
|
233
|
+
|
|
234
|
+
\`harmony_generate_prompt\` with \`cardId\` (or \`shortId\` plus \`projectId\`) and a \`variant\`:
|
|
235
|
+
\`execute\` (default) for well-defined work, \`analysis\` for unclear requirements, \`draft\` when
|
|
236
|
+
you want feedback on a design first. It returns role framing, focus areas, subtasks and links.
|
|
237
|
+
|
|
238
|
+
Then show the user the card: title, short id, priority, labels, due date, description, subtasks.
|
|
239
|
+
|
|
240
|
+
## 4. Implement, and check in at every milestone
|
|
241
|
+
|
|
242
|
+
Checkpoints: 20% explored · 50% implementing · 80% verifying · 100% done.
|
|
243
|
+
|
|
244
|
+
On the card itself, \`progressPercent\` and \`currentTask\` each overwrite one field, so the live
|
|
245
|
+
status shows only your latest checkpoint. The timeline keeps more: a checkpoint that carries both
|
|
246
|
+
a \`progressPercent\` and a \`currentTask\` different from the last one leaves a row saying what you
|
|
247
|
+
were **about to do**, and **each entry in \`actions\` leaves a row saying what you actually did** —
|
|
248
|
+
that is the evidence the team can still read afterwards.
|
|
249
|
+
|
|
250
|
+
\`\`\`
|
|
251
|
+
harmony_update_agent_progress({
|
|
252
|
+
cardId, agentIdentifier: "$AGENT_IDENTIFIER", agentName: "$AGENT_NAME",
|
|
253
|
+
progressPercent: 50,
|
|
254
|
+
currentTask: "Extracting refreshIfExpired() in auth.ts",
|
|
255
|
+
actions: [
|
|
256
|
+
{ description: "Read auth.ts and middleware/session.ts — the refresh path is duplicated in both, which is the actual bug" },
|
|
257
|
+
{ description: "Ruled out patching verifyToken(): three routes depend on its current behaviour" },
|
|
258
|
+
{ description: "Ran bun run lint — green, exit 0" },
|
|
259
|
+
],
|
|
260
|
+
status: "working", // or blocked / waiting / paused
|
|
261
|
+
blockers: [],
|
|
262
|
+
})
|
|
263
|
+
\`\`\`
|
|
264
|
+
|
|
265
|
+
Three to six entries per checkpoint, one sentence each; past 512 characters an entry is silently truncated. Say what you did,
|
|
266
|
+
not what you intend to do.
|
|
267
|
+
|
|
268
|
+
Right after each update, poll for steering:
|
|
269
|
+
|
|
270
|
+
\`\`\`
|
|
271
|
+
harmony_get_pending_messages({ cardId, sessionId, sinceSeq }) // sinceSeq starts at 0
|
|
272
|
+
\`\`\`
|
|
273
|
+
|
|
274
|
+
Fold any messages into the next step and advance \`sinceSeq\` to the largest \`seq\` returned. Two
|
|
275
|
+
flags come back and mean opposite things:
|
|
276
|
+
|
|
277
|
+
- \`stopped: true\` — a human pressed Stop. **Terminal.** Make no further edits, commits, pushes,
|
|
278
|
+
card moves, comments or progress writes; report what is finished and where any uncommitted work
|
|
279
|
+
lives.
|
|
280
|
+
- \`sessionStale: true\` — your session id is no longer live, usually the inactivity sweep. **Nobody stopped you.**
|
|
281
|
+
Carry on, with a new id: the **poll** opens nothing, so call \`harmony_start_agent_session\`
|
|
282
|
+
yourself; the **progress** call has already opened a replacement that inherited the steering
|
|
283
|
+
channel, so just take the new \`session.id\` from its reply.
|
|
284
|
+
|
|
285
|
+
If the two flags ever disagree, the stop wins.
|
|
286
|
+
|
|
287
|
+
Report findings and decisions with \`harmony_add_comment\` (\`commentType\`: \`question\` and
|
|
288
|
+
\`blocker\` signal that you need a human; \`decision\`, \`finding\`, \`summary\`, \`progress\`,
|
|
289
|
+
\`message\`), not by editing the card description.
|
|
290
|
+
|
|
291
|
+
## 5. Finish
|
|
292
|
+
|
|
293
|
+
\`\`\`
|
|
294
|
+
harmony_end_agent_session({ cardId, status: "completed", progressPercent: 100, moveToColumn: "Review" })
|
|
295
|
+
\`\`\`
|
|
296
|
+
|
|
297
|
+
One call: it moves the card, and on \`status: "completed"\` it also removes the \`agent\` label. Use
|
|
298
|
+
\`status: "paused"\` when you stop mid-flight — that leaves the label on, which is what you want.
|
|
299
|
+
|
|
300
|
+
Opened a PR? Attach it **after** the session end has moved the card, both ways:
|
|
301
|
+
\`harmony_add_external_link\` (durable — it survives a later description edit) and a
|
|
302
|
+
\`PR: <url>\` line in the description.
|
|
303
|
+
|
|
304
|
+
Then summarise what changed.
|
|
305
|
+
|
|
306
|
+
## Writing a plan
|
|
307
|
+
|
|
308
|
+
${HARMONY_PLAN_RULE}
|
|
309
|
+
|
|
310
|
+
## Worth knowing
|
|
311
|
+
|
|
312
|
+
- Moving a card to a terminal column ends your session — a column that marks cards done, or one
|
|
313
|
+
named \`done\`, \`completed\` or \`review\`. The response says \`sessionEnded\`.
|
|
314
|
+
- \`harmony_add_label_to_card\` and \`harmony_start_agent_session\`'s \`addLabels\` both CREATE a
|
|
315
|
+
label that does not exist yet. Check the spelling.
|
|
316
|
+
- \`harmony_add_comment\` works on any card you can see, including one you hold no session on.`;
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Render the workflow prompt for one runtime.
|
|
320
|
+
*
|
|
321
|
+
* The placeholders are explicit tokens rather than prose. The previous version
|
|
322
|
+
* chained `.replace("Your agent identifier", …)` over the prompt text, which
|
|
323
|
+
* substitutes only the FIRST occurrence and no-ops silently the moment that
|
|
324
|
+
* exact sentence is reworded — so a prompt could ship telling Cursor to call
|
|
325
|
+
* itself "Your agent name" with nothing failing (#1124).
|
|
326
|
+
*/
|
|
327
|
+
export function renderWorkflowPrompt(opts: {
|
|
328
|
+
cardArgument: string;
|
|
329
|
+
agentIdentifier: string;
|
|
330
|
+
agentName: string;
|
|
331
|
+
}): string {
|
|
332
|
+
return HARMONY_WORKFLOW_PROMPT.replaceAll("$ARGUMENTS", opts.cardArgument)
|
|
333
|
+
.replaceAll("$AGENT_IDENTIFIER", opts.agentIdentifier)
|
|
334
|
+
.replaceAll("$AGENT_NAME", opts.agentName);
|
|
335
|
+
}
|
package/src/tui/setup.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import {
|
|
3
4
|
existsSync,
|
|
@@ -23,7 +24,11 @@ import {
|
|
|
23
24
|
} from "../config.js";
|
|
24
25
|
import { loginWithBrowser, type OAuthTokens } from "../oauth-login.js";
|
|
25
26
|
import { onboardNewUser } from "../onboard.js";
|
|
26
|
-
import { buildSkillFile
|
|
27
|
+
import { buildSkillFile } from "../skills.js";
|
|
28
|
+
import {
|
|
29
|
+
HARMONY_AGENTS_SECTION,
|
|
30
|
+
renderWorkflowPrompt,
|
|
31
|
+
} from "./agent-instructions.js";
|
|
27
32
|
import { type AgentId, detectAgents } from "./agents.js";
|
|
28
33
|
import { confirmOrDefault, shouldAssumeYes } from "./confirm.js";
|
|
29
34
|
import { runDocsStep } from "./docs.js";
|
|
@@ -61,11 +66,17 @@ export interface SetupOptions {
|
|
|
61
66
|
* keep, especially under autonomous agent runs. New tools default to prompting
|
|
62
67
|
* until someone classifies them here (safe failure mode). `--allow-all-tools`
|
|
63
68
|
* overrides this with a blanket grant.
|
|
69
|
+
*
|
|
70
|
+
* Every name here must exist in the advertised `TOOLS` object in `server.ts`.
|
|
71
|
+
* `harmony_get_card_by_short_id` sat in this list for four minor versions after
|
|
72
|
+
* 2.14.0 dropped it, writing a permission rule for a tool that can never be
|
|
73
|
+
* offered — and the same dead name was being installed into AGENTS.md, where an
|
|
74
|
+
* agent read it and had to correct us. `setup-agent-files.test.ts` now fails on
|
|
75
|
+
* a name that is not advertised, in this list and in every generated file (#1124).
|
|
64
76
|
*/
|
|
65
|
-
const SAFE_HARMONY_TOOLS = [
|
|
77
|
+
export const SAFE_HARMONY_TOOLS = [
|
|
66
78
|
// Reads
|
|
67
79
|
"harmony_get_card",
|
|
68
|
-
"harmony_get_card_by_short_id",
|
|
69
80
|
"harmony_search_cards",
|
|
70
81
|
"harmony_get_board",
|
|
71
82
|
"harmony_get_context",
|
|
@@ -77,12 +88,19 @@ const SAFE_HARMONY_TOOLS = [
|
|
|
77
88
|
"harmony_get_comments",
|
|
78
89
|
"harmony_get_plan",
|
|
79
90
|
"harmony_list_plans",
|
|
91
|
+
"harmony_get_playbook",
|
|
92
|
+
"harmony_list_playbook",
|
|
80
93
|
"harmony_get_agent_session",
|
|
94
|
+
// The checkpoint steering poll. Absent until #1124, so every checkpoint of
|
|
95
|
+
// every run raised a prompt for a read the workflow itself prescribes.
|
|
96
|
+
"harmony_get_pending_messages",
|
|
81
97
|
"harmony_get_workspace_members",
|
|
82
98
|
"harmony_list_agents",
|
|
83
99
|
"harmony_resolve_links",
|
|
100
|
+
"harmony_suggest_relations",
|
|
84
101
|
"harmony_recall",
|
|
85
102
|
"harmony_memory_search",
|
|
103
|
+
"harmony_vault_index",
|
|
86
104
|
"harmony_generate_prompt",
|
|
87
105
|
// Routine writes
|
|
88
106
|
"harmony_create_card",
|
|
@@ -91,6 +109,7 @@ const SAFE_HARMONY_TOOLS = [
|
|
|
91
109
|
"harmony_assign_card",
|
|
92
110
|
"harmony_create_subtask",
|
|
93
111
|
"harmony_toggle_subtask",
|
|
112
|
+
"harmony_update_subtask",
|
|
94
113
|
"harmony_add_label_to_card",
|
|
95
114
|
"harmony_remove_label_from_card",
|
|
96
115
|
"harmony_create_label",
|
|
@@ -98,6 +117,10 @@ const SAFE_HARMONY_TOOLS = [
|
|
|
98
117
|
"harmony_update_comment",
|
|
99
118
|
"harmony_add_link_to_card",
|
|
100
119
|
"harmony_remove_link_from_card",
|
|
120
|
+
// The durable carrier for a PR link — the completion path calls it on every
|
|
121
|
+
// card that changed code, and it mirrors the card-link pair above.
|
|
122
|
+
"harmony_add_external_link",
|
|
123
|
+
"harmony_remove_external_link",
|
|
101
124
|
"harmony_start_agent_session",
|
|
102
125
|
"harmony_update_agent_progress",
|
|
103
126
|
"harmony_end_agent_session",
|
|
@@ -112,6 +135,9 @@ const SAFE_HARMONY_TOOLS = [
|
|
|
112
135
|
"harmony_remember",
|
|
113
136
|
"harmony_relate",
|
|
114
137
|
"harmony_update_memory",
|
|
138
|
+
// A ranking counter on a recalled memory. A write, not a read — it never
|
|
139
|
+
// deletes, hides or supersedes anything.
|
|
140
|
+
"harmony_recall_feedback",
|
|
115
141
|
"harmony_process_command",
|
|
116
142
|
"harmony_sync",
|
|
117
143
|
];
|
|
@@ -401,7 +427,8 @@ export async function resolveProjectSlug(
|
|
|
401
427
|
export interface FileToWrite {
|
|
402
428
|
path: string;
|
|
403
429
|
content: string;
|
|
404
|
-
|
|
430
|
+
/** `markdown` merges a Harmony-owned section into a file the project owns. */
|
|
431
|
+
type: "text" | "json" | "toml" | "markdown";
|
|
405
432
|
tomlSection?: string;
|
|
406
433
|
mode?: number;
|
|
407
434
|
}
|
|
@@ -512,64 +539,15 @@ async function getAgentFiles(
|
|
|
512
539
|
}
|
|
513
540
|
|
|
514
541
|
case "codex": {
|
|
515
|
-
// AGENTS.md
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
## Agent identity — always identify as yourself
|
|
521
|
-
|
|
522
|
-
Every \`harmony_start_agent_session\` call passes \`agentIdentifier\` + \`agentName\`. **Use your own
|
|
523
|
-
identity, never a hardcoded one from this file.** AGENTS.md is a cross-runtime convention file, so
|
|
524
|
-
more than one kind of agent will read it; the board shows agents as teammates, and a session
|
|
525
|
-
attributed to the wrong runtime misattributes the work in front of the whole team.
|
|
526
|
-
|
|
527
|
-
- \`agentIdentifier\` — a stable kebab-case id for the runtime you actually are
|
|
528
|
-
- \`agentName\` — its human-readable name
|
|
529
|
-
|
|
530
|
-
Known values: \`claude-code\` / "Claude Code", \`codex\` / "OpenAI Codex", \`cursor\` / "Cursor",
|
|
531
|
-
\`claude-desktop\` / "Claude Desktop". If you are a runtime not listed here, use your own name rather
|
|
532
|
-
than borrowing the closest entry.
|
|
533
|
-
|
|
534
|
-
## Starting Work on a Card
|
|
535
|
-
|
|
536
|
-
When given a card reference (e.g., #42 or a card name), follow this workflow:
|
|
537
|
-
|
|
538
|
-
1. Use \`harmony_get_card\` or \`harmony_search_cards\` to find the card
|
|
539
|
-
2. Move the card to "In Progress" using \`harmony_move_card\`
|
|
540
|
-
3. Add the "agent" label using \`harmony_add_label_to_card\`
|
|
541
|
-
4. Start a session with \`harmony_start_agent_session\`, passing **your own** \`agentIdentifier\` +
|
|
542
|
-
\`agentName\` (see "Agent identity" above)
|
|
543
|
-
5. Show the card details to the user
|
|
544
|
-
6. Use \`harmony_generate_prompt\` to get guidance, then implement the solution
|
|
545
|
-
7. Update progress periodically with \`harmony_update_agent_progress\`
|
|
546
|
-
8. When done, call \`harmony_end_agent_session\` and move to "Review"
|
|
547
|
-
|
|
548
|
-
## Auto-Detect Card for Implementation Tasks
|
|
549
|
-
|
|
550
|
-
Before implementing a plan or feature, check if it maps to an existing Harmony card:
|
|
551
|
-
|
|
552
|
-
1. Use \`harmony_search_cards\` with keywords from the task description
|
|
553
|
-
2. If a match is found, call \`harmony_start_agent_session\` with **your own** \`agentIdentifier\` +
|
|
554
|
-
\`agentName\` (see "Agent identity" above), plus \`moveToColumn: "In Progress"\`, \`addLabels: ["agent"]\`
|
|
555
|
-
3. Update progress with \`harmony_update_agent_progress\` at milestones
|
|
556
|
-
4. When done, call \`harmony_end_agent_session\` with status: "completed", moveToColumn: "Review"
|
|
557
|
-
|
|
558
|
-
Skip if: work was already started with a card reference, or no matching card exists.
|
|
559
|
-
|
|
560
|
-
## Available Harmony Tools
|
|
561
|
-
|
|
562
|
-
- \`harmony_get_card\`, \`harmony_get_card_by_short_id\`, \`harmony_search_cards\` - Find cards
|
|
563
|
-
- \`harmony_move_card\` - Move cards between columns
|
|
564
|
-
- \`harmony_add_label_to_card\`, \`harmony_remove_label_from_card\` - Manage labels
|
|
565
|
-
- \`harmony_start_agent_session\`, \`harmony_update_agent_progress\`, \`harmony_end_agent_session\` - Track work
|
|
566
|
-
- \`harmony_get_board\` - Get board state
|
|
567
|
-
- \`harmony_generate_prompt\` - Get role-based guidance and focus areas for the card
|
|
568
|
-
`;
|
|
542
|
+
// The Harmony section of AGENTS.md, written as type "markdown" so it is
|
|
543
|
+
// MERGED into whatever the project already has: the docs-step scaffold
|
|
544
|
+
// written earlier in this same run, or a file the user maintains by hand.
|
|
545
|
+
// A plain text write destroyed both — setup forces writes on a fresh
|
|
546
|
+
// install, and this push lands after the scaffold's (#1124).
|
|
569
547
|
files.push({
|
|
570
548
|
path: join(cwd, "AGENTS.md"),
|
|
571
|
-
content:
|
|
572
|
-
type: "
|
|
549
|
+
content: HARMONY_AGENTS_SECTION,
|
|
550
|
+
type: "markdown",
|
|
573
551
|
});
|
|
574
552
|
|
|
575
553
|
// Codex prompt file
|
|
@@ -582,7 +560,7 @@ arguments:
|
|
|
582
560
|
required: true
|
|
583
561
|
---
|
|
584
562
|
|
|
585
|
-
${
|
|
563
|
+
${renderWorkflowPrompt({ cardArgument: "{{card}}", agentIdentifier: "codex", agentName: "OpenAI Codex" })}
|
|
586
564
|
`;
|
|
587
565
|
|
|
588
566
|
if (installMode === "global") {
|
|
@@ -652,7 +630,7 @@ alwaysApply: false
|
|
|
652
630
|
|
|
653
631
|
When the user asks you to work on a Harmony card (references like #42, card names, or UUIDs):
|
|
654
632
|
|
|
655
|
-
${
|
|
633
|
+
${renderWorkflowPrompt({ cardArgument: "the card reference", agentIdentifier: "cursor", agentName: "Cursor" })}
|
|
656
634
|
`;
|
|
657
635
|
|
|
658
636
|
if (installMode === "global") {
|
|
@@ -708,7 +686,7 @@ description: Activate when user asks to work on a Harmony card (references like
|
|
|
708
686
|
|
|
709
687
|
When working on a Harmony card:
|
|
710
688
|
|
|
711
|
-
${
|
|
689
|
+
${renderWorkflowPrompt({ cardArgument: "the card reference", agentIdentifier: "windsurf", agentName: "Windsurf" })}
|
|
712
690
|
`;
|
|
713
691
|
|
|
714
692
|
if (installMode === "global") {
|
|
@@ -1576,6 +1554,11 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1576
1554
|
` ${colors.success("\u2713")} ${colors.dim(formatPath(writtenLocalConfigPath, home))} ${colors.dim("(created)")}`,
|
|
1577
1555
|
);
|
|
1578
1556
|
|
|
1557
|
+
// The scan runs AFTER the global mirror below, deliberately: it spawns a
|
|
1558
|
+
// child with inherited stdio for as long as this repo's test suite takes,
|
|
1559
|
+
// and a Ctrl-C there must not cost the operator the global default that
|
|
1560
|
+
// every server started outside this directory reads (#893).
|
|
1561
|
+
//
|
|
1579
1562
|
// Mirror the choice into the GLOBAL default, explicitly (#893). The local
|
|
1580
1563
|
// file above already pins this directory; this second write is what gives a
|
|
1581
1564
|
// server started elsewhere — Claude Desktop, another repo — a context at
|
|
@@ -1592,6 +1575,8 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1592
1575
|
{ global: true },
|
|
1593
1576
|
);
|
|
1594
1577
|
}
|
|
1578
|
+
|
|
1579
|
+
await offerCommandScan(dirname(writtenLocalConfigPath), assumeYes);
|
|
1595
1580
|
}
|
|
1596
1581
|
|
|
1597
1582
|
// Step 10: Show completion message
|
|
@@ -1686,3 +1671,99 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1686
1671
|
}
|
|
1687
1672
|
console.log("");
|
|
1688
1673
|
}
|
|
1674
|
+
|
|
1675
|
+
/**
|
|
1676
|
+
* The command that proposes a repo's `commands` block by running it.
|
|
1677
|
+
*
|
|
1678
|
+
* `@latest` is this repo's own convention for an `npx` spec (`@gethmy/mcp@latest`
|
|
1679
|
+
* in every documented invocation) and it is load-bearing here: `scan-commands`
|
|
1680
|
+
* arrives in a release later than several already published, and a warm npx
|
|
1681
|
+
* cache of an older `@gethmy/agent` would otherwise hand the operator the
|
|
1682
|
+
* "may predate it" fallback forever with no way to see why.
|
|
1683
|
+
*/
|
|
1684
|
+
const SCAN_ARGV = ["--yes", "@gethmy/agent@latest", "scan-commands"] as const;
|
|
1685
|
+
|
|
1686
|
+
function scanTip(): void {
|
|
1687
|
+
console.log(
|
|
1688
|
+
` ${colors.dim("Tip: run")} ${colors.highlight(`npx ${SCAN_ARGV.slice(1).join(" ")}`)} ${colors.dim("to have the daemon prove which build, test and dev commands this repo has, and write them into the pin.")}`,
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
/**
|
|
1693
|
+
* Offer to scan this repo's commands, and run the scanner when asked (#1116).
|
|
1694
|
+
*
|
|
1695
|
+
* Card 1116's first acceptance criterion is that the scanner RUNS when a
|
|
1696
|
+
* project is set up — this is that moment, and a printed tip is not a run.
|
|
1697
|
+
* What this package must not do is scan by itself: the scan executes the
|
|
1698
|
+
* repo's own build, test and dev commands, and `@gethmy/mcp` neither depends
|
|
1699
|
+
* on `@gethmy/harness` (which owns both the execution and the schema) nor
|
|
1700
|
+
* spawns a toolchain anywhere else. So it does what the tip asked a person to
|
|
1701
|
+
* do — spawn the agent CLI — which keeps the dependency direction intact
|
|
1702
|
+
* (agent → harness → shared, mcp beside them) and re-types no schema.
|
|
1703
|
+
*
|
|
1704
|
+
* Three properties, each deliberate.
|
|
1705
|
+
*
|
|
1706
|
+
* **Asked, never assumed.** The scan starts this repo's build, test and dev
|
|
1707
|
+
* commands, which is minutes of work and arbitrary code from the checkout. A
|
|
1708
|
+
* person is present at setup and is the right one to say yes.
|
|
1709
|
+
*
|
|
1710
|
+
* **Non-interactive setups are not scanned at all** — `--yes`, a pipe, a
|
|
1711
|
+
* coding agent — even though `--yes` means yes to everything else. Those two
|
|
1712
|
+
* are not the same question: the other confirmations write files this command
|
|
1713
|
+
* already owns, and this one hands control to another package's process for
|
|
1714
|
+
* an unbounded time. They get the tip, i.e. exactly the behaviour that
|
|
1715
|
+
* shipped before this.
|
|
1716
|
+
*
|
|
1717
|
+
* **A failure is not an error.** The published `@gethmy/agent` gains this
|
|
1718
|
+
* subcommand only on its next release, and `npx` needs a network. Either way
|
|
1719
|
+
* the fallback is the tip, so an older agent degrades to the previous
|
|
1720
|
+
* behaviour rather than turning setup red over an optional convenience.
|
|
1721
|
+
*
|
|
1722
|
+
* It deliberately does NOT pass `--write`: the proposal is printed for a
|
|
1723
|
+
* person to read, which is card 1116's fourth criterion (never applied
|
|
1724
|
+
* silently), and `scan-commands --write` is the separate, explicit act.
|
|
1725
|
+
*/
|
|
1726
|
+
export async function offerCommandScan(
|
|
1727
|
+
repoDir: string,
|
|
1728
|
+
assumeYes: boolean,
|
|
1729
|
+
): Promise<void> {
|
|
1730
|
+
if (assumeYes) {
|
|
1731
|
+
scanTip();
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
const scan = await confirmOrDefault(assumeYes, {
|
|
1736
|
+
message:
|
|
1737
|
+
"Scan this repo's commands now? It runs your build, test and dev scripts once to prove which ones exist. Nothing is written.",
|
|
1738
|
+
initialValue: true,
|
|
1739
|
+
});
|
|
1740
|
+
// Ctrl-C ends setup, as it does at every other prompt in this file. A
|
|
1741
|
+
// cancel folded into "no" would carry on through the remaining steps as if
|
|
1742
|
+
// the person had answered, which is the one reading they did not give.
|
|
1743
|
+
if (p.isCancel(scan)) {
|
|
1744
|
+
p.cancel("Setup cancelled.");
|
|
1745
|
+
process.exit(0);
|
|
1746
|
+
}
|
|
1747
|
+
if (!scan) {
|
|
1748
|
+
scanTip();
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
console.log(
|
|
1753
|
+
` ${colors.dim("Running")} ${colors.highlight(`npx ${SCAN_ARGV.slice(1).join(" ")}`)}${colors.dim(" …")}`,
|
|
1754
|
+
);
|
|
1755
|
+
const result = spawnSync("npx", [...SCAN_ARGV], {
|
|
1756
|
+
cwd: repoDir,
|
|
1757
|
+
stdio: "inherit",
|
|
1758
|
+
});
|
|
1759
|
+
if (result.error || result.status !== 0) {
|
|
1760
|
+
console.log(
|
|
1761
|
+
` ${colors.dim("The scan did not run — your installed @gethmy/agent may predate it.")}`,
|
|
1762
|
+
);
|
|
1763
|
+
scanTip();
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
console.log(
|
|
1767
|
+
` ${colors.dim("Re-run it with")} ${colors.highlight("--write")} ${colors.dim("to merge that block into the pin.")}`,
|
|
1768
|
+
);
|
|
1769
|
+
}
|