@thehammer/danx-dashboard-mcp 0.1.51 → 0.1.52
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 +5 -4
- package/dist/handlers.js +111 -2
- package/dist/index.js +53 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,13 +23,14 @@ All exposed as `mcp__danx-dashboard__<name>` once wired through the workspace `m
|
|
|
23
23
|
|---|---|---|
|
|
24
24
|
| `issue_list` | `GET /api/issues` | filters: `type`, `parent_id` (null → root-only), `dispatchable_derived`, `assigned_agent`, `include_closed`, `limit`, `offset` |
|
|
25
25
|
| `issue_get` | `GET /api/issues/:id` | Returns hydrated card + ancestor chain |
|
|
26
|
-
| `issue_create` | `POST /api/issues` | Epic REQUIRES non-empty `phase_children[]` (atomic insert) |
|
|
27
|
-
| `issue_edit` | `PATCH /api/issues/:id/edit` | Prose + structured keys (`title`, `description`, `ac`, `checklists`, `effort_level`, `parent_id`, `priority`, `list_id`); semantic keys refused with 400 + pointer to dedicated handler. `priority` (DX-1532) takes a tier word (`low`/`high`/…) or a number in `[0,6)` — the ONLY way to set the numeric column the Trello label + dashboard badge read; never set priority via description prose |
|
|
28
|
-
| `issue_transition` | `POST /api/issues/:id/transition` | Actions: ready, pickup, rollback_pickup, complete, cancel, block, unblock, archive, reopen |
|
|
26
|
+
| `issue_create` | `POST /api/issues` | Epic REQUIRES non-empty `phase_children[]` (atomic insert). `title` = short domain-naming label; `summary` = 1–3 plain-language sentences, always shown; `description` = the collapsed "Context" body. Root and every phase child take their own `summary` |
|
|
27
|
+
| `issue_edit` | `PATCH /api/issues/:id/edit` | Prose + structured keys (`title`, `summary` (null clears), `description`, `ac`, `checklists`, `effort_level`, `parent_id`, `priority`, `list_id`); semantic keys refused with 400 + pointer to dedicated handler. `priority` (DX-1532) takes a tier word (`low`/`high`/…) or a number in `[0,6)` — the ONLY way to set the numeric column the Trello label + dashboard badge read; never set priority via description prose |
|
|
28
|
+
| `issue_transition` | `POST /api/issues/:id/transition` | Actions: ready, pickup, rollback_pickup, complete, cancel, block, unblock, archive, reopen. A successful `block` also returns `solutions_reminder: {solution_count, instruction}` |
|
|
29
|
+
| `issue_solution` | `GET/POST/PATCH/DELETE /api/issues/:id/solutions[/:sid]` | Actions list / add / edit / remove. Edit + remove are hash-guarded (`base_hash`, 409 `stale_solution` with the current row); at most one live `recommended`; a chosen option cannot be edited. No answer action — the operator answers in the dashboard |
|
|
29
30
|
| `issue_triage` | `POST /api/issues/:id/triage` | Send `{confidence, reason}` — an integer 0-5 score; the server computes the verdict (approve/cancel/keep/defer) against the board's configured thresholds (DX-2086). `keep`/`defer` now block the card. None of these are a cross-card ordering gate; use `issue_dependency` to sequence cards |
|
|
30
31
|
| `issue_comment` | `POST/PATCH/DELETE /api/issues/:id/comments[/:cid]` | Author server-stamped, soft-delete preserved |
|
|
31
32
|
| `issue_dependency` | `POST/DELETE /api/issues/:id/dependencies[/:did]` | `depends_on` cycle-checked; remove hardcodes `reason: "recorded_in_error"`. The only mechanism the dispatch picker enforces to sequence one card after another — status alone is not a substitute |
|
|
32
|
-
| `issue_requires_human` | `POST/DELETE /api/issues/:id/requires-human` | Set replaces step rows atomically; clear soft-deletes them |
|
|
33
|
+
| `issue_requires_human` | `POST/DELETE /api/issues/:id/requires-human` | Set replaces step rows atomically; clear soft-deletes them. A successful set also returns `solutions_reminder: {solution_count, instruction}` |
|
|
33
34
|
| `issue_retro` | `PUT /api/issues/:id/retro` | Requires terminal card; replace semantics |
|
|
34
35
|
|
|
35
36
|
## Build + test
|
package/dist/handlers.js
CHANGED
|
@@ -158,6 +158,8 @@ export async function issueCreate(client, args, defaultBoard) {
|
|
|
158
158
|
title: args.title,
|
|
159
159
|
description: args.description,
|
|
160
160
|
};
|
|
161
|
+
if (args.summary !== undefined)
|
|
162
|
+
body.summary = args.summary;
|
|
161
163
|
if (args.parent_id !== undefined)
|
|
162
164
|
body.parent_id = args.parent_id;
|
|
163
165
|
if (args.ac !== undefined)
|
|
@@ -205,12 +207,63 @@ export async function issueEdit(client, args) {
|
|
|
205
207
|
}
|
|
206
208
|
export async function issueTransition(client, args) {
|
|
207
209
|
const { id, board, ...body } = args;
|
|
208
|
-
|
|
210
|
+
const result = await client.request({
|
|
209
211
|
method: "POST",
|
|
210
212
|
path: `/${encodeURIComponent(id)}/transition`,
|
|
211
213
|
body,
|
|
212
214
|
board,
|
|
213
215
|
});
|
|
216
|
+
return args.action === "block" ? withSolutionsReminder(client, id, board, result) : result;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Attach the solutions reminder to a SUCCESSFUL gating write.
|
|
220
|
+
*
|
|
221
|
+
* ENFORCED BY FEEDBACK, NOT BY REFUSAL. The server never rejects a block or a
|
|
222
|
+
* requires-human hold for lacking solutions: some holds legitimately have no
|
|
223
|
+
* options to list (a credential only the operator can supply), and a refusal
|
|
224
|
+
* there would push the agent into inventing solutions to get past the gate.
|
|
225
|
+
* What the operator needs is that an agent which CAN lay out options always
|
|
226
|
+
* does — so the reminder rides the success response, where it is read at the
|
|
227
|
+
* exact moment the agent is deciding whether it is done.
|
|
228
|
+
*
|
|
229
|
+
* The gating write's own envelope is returned untouched beside it. A refused
|
|
230
|
+
* write gets no reminder (there is no stop to remind about). A failure READING
|
|
231
|
+
* the solutions is reported inside the reminder rather than thrown: the gating
|
|
232
|
+
* write already succeeded, and throwing would tell the agent its block failed
|
|
233
|
+
* when it did not — the failure is surfaced, never swallowed.
|
|
234
|
+
*/
|
|
235
|
+
export async function withSolutionsReminder(client, id, board, result) {
|
|
236
|
+
if (!result.ok)
|
|
237
|
+
return result;
|
|
238
|
+
const nextStep = `issue_solution({id: "${id}", action: "add", title, body, pro, con})`;
|
|
239
|
+
let listed;
|
|
240
|
+
try {
|
|
241
|
+
listed = await client.request({
|
|
242
|
+
method: "GET",
|
|
243
|
+
path: `/${encodeURIComponent(id)}/solutions`,
|
|
244
|
+
board,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
return { ...result, solutions_reminder: unreadableReminder(id, err instanceof Error ? err.message : String(err)) };
|
|
249
|
+
}
|
|
250
|
+
// `body` is the server's JSON verbatim — `null` is valid JSON, so read it null-safely;
|
|
251
|
+
// a throw here would land OUTSIDE the try above and misreport the successful write.
|
|
252
|
+
const solutions = listed.ok ? listed.body?.solutions : undefined;
|
|
253
|
+
if (!Array.isArray(solutions)) {
|
|
254
|
+
return { ...result, solutions_reminder: unreadableReminder(id, `HTTP ${listed.status}`) };
|
|
255
|
+
}
|
|
256
|
+
const count = solutions.length;
|
|
257
|
+
const instruction = count === 0
|
|
258
|
+
? `NO SOLUTIONS ARE LISTED ON ${id}. Before you stop, add EVERY viable solution with ${nextStep}, and mark the one you recommend with recommended: true. The operator answers a stopped card by picking a listed solution — with none listed, they have to reconstruct the options from the description.`
|
|
259
|
+
: `${count} solution${count === 1 ? " is" : "s are"} listed on ${id}. Before you stop, confirm EVERY viable solution is on the card and add any that are missing with ${nextStep}. The operator can only pick from what is listed.`;
|
|
260
|
+
return { ...result, solutions_reminder: { solution_count: count, instruction } };
|
|
261
|
+
}
|
|
262
|
+
function unreadableReminder(id, detail) {
|
|
263
|
+
return {
|
|
264
|
+
solution_count: null,
|
|
265
|
+
instruction: `Could not read the solutions on ${id} (${detail}). Check with issue_solution({id: "${id}", action: "list"}) and make sure EVERY viable solution is listed before you stop.`,
|
|
266
|
+
};
|
|
214
267
|
}
|
|
215
268
|
export async function issueTriage(client, args) {
|
|
216
269
|
const { id, board, ...body } = args;
|
|
@@ -400,6 +453,61 @@ export async function issueChecklist(client, args) {
|
|
|
400
453
|
}
|
|
401
454
|
}
|
|
402
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* A card's candidate solutions via `/api/issues/:id/solutions[/:sid]`,
|
|
458
|
+
* action-dispatched like `issue_checklist`. A missing required arg for the
|
|
459
|
+
* chosen action throws at this boundary (no round-trip); the server's refusal
|
|
460
|
+
* envelopes (`stale_solution` with the current row, a second recommendation,
|
|
461
|
+
* editing a chosen option) pass through verbatim.
|
|
462
|
+
*
|
|
463
|
+
* There is deliberately NO answer action. Answering a card releases the human
|
|
464
|
+
* gates on it — an agent that could answer its own question could release the
|
|
465
|
+
* very stop it set to wait for a human. The operator answers in the dashboard.
|
|
466
|
+
*/
|
|
467
|
+
export async function issueSolution(client, args) {
|
|
468
|
+
const base = `/${encodeURIComponent(args.id)}/solutions`;
|
|
469
|
+
const board = args.board;
|
|
470
|
+
const content = {};
|
|
471
|
+
for (const key of ["title", "body", "pro", "con", "recommended"]) {
|
|
472
|
+
if (args[key] !== undefined)
|
|
473
|
+
content[key] = args[key];
|
|
474
|
+
}
|
|
475
|
+
switch (args.action) {
|
|
476
|
+
case "list":
|
|
477
|
+
return client.request({ method: "GET", path: base, board });
|
|
478
|
+
case "add":
|
|
479
|
+
if (typeof args.title !== "string") {
|
|
480
|
+
throw new Error("issue_solution action=add requires title");
|
|
481
|
+
}
|
|
482
|
+
return client.request({ method: "POST", path: base, body: content, board });
|
|
483
|
+
case "edit":
|
|
484
|
+
if (args.solution_id === undefined) {
|
|
485
|
+
throw new Error("issue_solution action=edit requires solution_id");
|
|
486
|
+
}
|
|
487
|
+
if (typeof args.base_hash !== "string") {
|
|
488
|
+
throw new Error("issue_solution action=edit requires base_hash");
|
|
489
|
+
}
|
|
490
|
+
return client.request({
|
|
491
|
+
method: "PATCH",
|
|
492
|
+
path: `${base}/${args.solution_id}`,
|
|
493
|
+
body: { base_hash: args.base_hash, ...content },
|
|
494
|
+
board,
|
|
495
|
+
});
|
|
496
|
+
case "remove":
|
|
497
|
+
if (args.solution_id === undefined) {
|
|
498
|
+
throw new Error("issue_solution action=remove requires solution_id");
|
|
499
|
+
}
|
|
500
|
+
if (typeof args.base_hash !== "string") {
|
|
501
|
+
throw new Error("issue_solution action=remove requires base_hash");
|
|
502
|
+
}
|
|
503
|
+
return client.request({
|
|
504
|
+
method: "DELETE",
|
|
505
|
+
path: `${base}/${args.solution_id}`,
|
|
506
|
+
body: { base_hash: args.base_hash },
|
|
507
|
+
board,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
403
511
|
export async function issueRequiresHuman(client, args) {
|
|
404
512
|
const idEnc = encodeURIComponent(args.id);
|
|
405
513
|
const board = args.board;
|
|
@@ -410,12 +518,13 @@ export async function issueRequiresHuman(client, args) {
|
|
|
410
518
|
if (!Array.isArray(args.steps)) {
|
|
411
519
|
throw new Error("issue_requires_human set=true requires steps[]");
|
|
412
520
|
}
|
|
413
|
-
|
|
521
|
+
const result = await client.request({
|
|
414
522
|
method: "POST",
|
|
415
523
|
path: `/${idEnc}/requires-human`,
|
|
416
524
|
body: { reason: args.reason, steps: args.steps },
|
|
417
525
|
board,
|
|
418
526
|
});
|
|
527
|
+
return withSolutionsReminder(client, args.id, board, result);
|
|
419
528
|
}
|
|
420
529
|
return client.request({
|
|
421
530
|
method: "DELETE",
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* - issue_triage POST /api/issues/:id/triage
|
|
20
20
|
* - issue_comment POST/PATCH/DELETE /api/issues/:id/comments[/:cid]
|
|
21
21
|
* - issue_checklist POST/PATCH/DELETE /api/issues/:id/checklists[/:cid[/items[/:iid]]]
|
|
22
|
+
* - issue_solution GET/POST/PATCH/DELETE /api/issues/:id/solutions[/:sid]
|
|
22
23
|
* - issue_dependency POST/DELETE /api/issues/:id/dependencies[/:did]
|
|
23
24
|
* - issue_requires_human POST/DELETE /api/issues/:id/requires-human
|
|
24
25
|
* - issue_quality_gate POST /api/issues/:id/quality-gates/:gate
|
|
@@ -79,7 +80,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
79
80
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
80
81
|
import { z } from "zod";
|
|
81
82
|
import { DashboardHttpClient } from "./http-client.js";
|
|
82
|
-
import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueQualityGate, issueQualityGateVerdict, issueRequiresHuman, issueRetro, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, planAddCard, planAddRecord, planConnect, planCreate, planDeleteRecord, planGet, planGetRecord, planList, planSetArchitecture, planUpdateRecord, repoKnowledgeGet, repoKnowledgeSet, } from "./handlers.js";
|
|
83
|
+
import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueQualityGate, issueQualityGateVerdict, issueRequiresHuman, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, planAddCard, planAddRecord, planConnect, planCreate, planDeleteRecord, planGet, planGetRecord, planList, planSetArchitecture, planUpdateRecord, repoKnowledgeGet, repoKnowledgeSet, } from "./handlers.js";
|
|
83
84
|
import { PRIORITY_TIER_WORDS } from "./priority.js";
|
|
84
85
|
function readEnvOrDie(name) {
|
|
85
86
|
const v = process.env[name];
|
|
@@ -195,6 +196,7 @@ const NON_EPIC_TYPES = ["Bug", "Feature", "Story", "Chore", "Task"];
|
|
|
195
196
|
// server 400, not silently.
|
|
196
197
|
const LIST_FIELD_GROUPS = [
|
|
197
198
|
"description",
|
|
199
|
+
"solutions",
|
|
198
200
|
"ac",
|
|
199
201
|
"comments",
|
|
200
202
|
"retro",
|
|
@@ -255,6 +257,12 @@ const boardField = {
|
|
|
255
257
|
.optional()
|
|
256
258
|
.describe("Target another board by its qualified id `<repo>:<slug>` (e.g. `platform:the-supply-operations-hub`); omit to use this dispatch's board. Unknown board → 404."),
|
|
257
259
|
};
|
|
260
|
+
// The three prose fields of a card, each with ONE job. Shared by issue_create
|
|
261
|
+
// (root + phase children) and issue_edit so the guidance an agent reads is
|
|
262
|
+
// identical wherever it writes the field.
|
|
263
|
+
const TITLE_DESCRIBE = 'Short, specific label that names the domain, so a reader recognises the card without opening it (e.g. "Guest checkout rejects carts holding a gift card"). Never a generic phrase like "2 real decisions needed", "Fix bug" or "Follow-up".';
|
|
264
|
+
const SUMMARY_DESCRIBE = "1–3 plain-language sentences — no markdown, no jargon — for someone who has never seen this codebase: what the card is about and why it matters. Always shown, never collapsed. It must stand on its own: not a second title, not a teaser for the description.";
|
|
265
|
+
const DESCRIPTION_DESCRIBE = 'The full body ("Context"): evidence, examples, technical detail. Long and markdown is normal; UIs collapse it by default. Candidate answers to the question a card is stopped on do NOT go here — list each one with issue_solution.';
|
|
258
266
|
// ---------------- issue_list ----------------
|
|
259
267
|
server.tool("issue_list", "List issues for the dispatch's board by default via GET /api/issues. Board-scoped; defaults to the dispatch's board. Pass `board` (a qualified id `<repo>:<slug>`) to list another board instead (unknown board → 404). Nested envelope (DX-935 / DX-937 — hard-cut, no flat params): `filter` — the OLD flat filters, now nested (type, parent_id, dispatchable_derived, status_derived[], self_dispatchable_derived, assigned_agent, include_closed, include_deleted, and `q` — free-text over id+title+description, the former standalone `q` param now lives at `filter.q`). `fields` — opt-in named field-GROUPS (description, ac, comments, retro, dependencies, triage, requires_human, assignment, quality_gates, children, effort); THE DEFAULT RESPONSE (no `fields`) IS MINIMAL — only cheap scalar columns (id, type, title, status, parent_id, priority, created_at, updated_at, assigned_agent), zero joins. Point any heavy read (full description, comments[], retro, ac items, dependency edges, triage history, quality-gate rows, children ids) at the matching `fields` entry rather than assuming it's already on the row. `sort` — ordered [{column, order}] (id|priority|repo_name|title|type|status_derived|triage_ice_total|created_at|updated_at); absent → default order (priority desc, repo_name asc) with an always-appended numeric-id tiebreaker (DX-10 follows DX-9). `limit`/`offset` — optional paging (no cap by default). Use issue_get for a single fully-detailed card.", {
|
|
260
268
|
filter: z
|
|
@@ -274,26 +282,27 @@ server.tool("issue_list", "List issues for the dispatch's board by default via G
|
|
|
274
282
|
fields: z
|
|
275
283
|
.array(z.enum(LIST_FIELD_GROUPS))
|
|
276
284
|
.optional()
|
|
277
|
-
.describe("Opt-in field-GROUPS to add to the minimal default row: description, ac, comments, retro, dependencies, triage, requires_human, assignment, quality_gates, children, effort. Absent/empty = minimal scalars only — no joins."),
|
|
285
|
+
.describe("Opt-in field-GROUPS to add to the minimal default row: description (description + summary), solutions (solutions_count), ac, comments, retro, dependencies, triage, requires_human, assignment, quality_gates, children, effort. Absent/empty = minimal scalars only — no joins."),
|
|
278
286
|
sort: sortField,
|
|
279
287
|
limit: z.number().int().positive().max(1000).optional(),
|
|
280
288
|
offset: z.number().int().nonnegative().optional(),
|
|
281
289
|
...boardField,
|
|
282
290
|
}, async (args) => jsonResult(await issueList(client, args)));
|
|
283
291
|
// ---------------- issue_get ----------------
|
|
284
|
-
server.tool("issue_get", "Fetch a single issue via GET /api/issues/:id. Board-scoped; defaults to the dispatch's board. Issue ids are globally unique, so this resolves from any dispatch regardless of `board`. DEFAULT RESPONSE IS MINIMAL (DX-935 / DX-937) — only cheap scalar columns (id, type, title, status, parent_id, priority, created_at, updated_at, assigned_agent); no joined collections. Pass `fields` to opt into named field-GROUPS: description (full description body), ac (acceptance-criteria + checklists model), comments (comments[]), retro (retro good/bad/action_items/commits), dependencies (waiting_on/conflict_on/blocked gate state), triage (triage history + ICE), requires_human (the requires_human gate + steps), assignment (dispatch/assigned_agent/lifecycle timestamps), quality_gates (DX-1177 — one row per registered gate {gate, required, status pending|pass|fail, completed_at, message}; a required PRE gate not yet `pass` pre-empts the work dispatch, and `issue_transition complete` refuses while a required POST gate row != pass), children (child id list + rollups), mirrors (external mirror sync state), code_review_items (code-review findings). Point any heavy read at the matching `fields` entry rather than assuming it's already on the row. 404 envelope on unknown id.", {
|
|
292
|
+
server.tool("issue_get", "Fetch a single issue via GET /api/issues/:id. Board-scoped; defaults to the dispatch's board. Issue ids are globally unique, so this resolves from any dispatch regardless of `board`. DEFAULT RESPONSE IS MINIMAL (DX-935 / DX-937) — only cheap scalar columns (id, type, title, status, parent_id, priority, created_at, updated_at, assigned_agent); no joined collections. Pass `fields` to opt into named field-GROUPS: description (full description body + the plain-language summary), solutions (the card's live candidate solutions, each with its content_hash, + every operator answer in decisions[]), ac (acceptance-criteria + checklists model), comments (comments[]), retro (retro good/bad/action_items/commits), dependencies (waiting_on/conflict_on/blocked gate state), triage (triage history + ICE), requires_human (the requires_human gate + steps), assignment (dispatch/assigned_agent/lifecycle timestamps), quality_gates (DX-1177 — one row per registered gate {gate, required, status pending|pass|fail, completed_at, message}; a required PRE gate not yet `pass` pre-empts the work dispatch, and `issue_transition complete` refuses while a required POST gate row != pass), children (child id list + rollups), mirrors (external mirror sync state), code_review_items (code-review findings). Point any heavy read at the matching `fields` entry rather than assuming it's already on the row. 404 envelope on unknown id.", {
|
|
285
293
|
id: z.string().min(1),
|
|
286
294
|
fields: z
|
|
287
295
|
.array(z.enum(GET_FIELD_GROUPS))
|
|
288
296
|
.optional()
|
|
289
|
-
.describe("Opt-in field-GROUPS to add to the minimal default row: description, ac, comments, retro, dependencies, triage, requires_human, assignment, quality_gates, children, mirrors, code_review_items. Absent/empty = minimal scalars only."),
|
|
297
|
+
.describe("Opt-in field-GROUPS to add to the minimal default row: description, solutions, ac, comments, retro, dependencies, triage, requires_human, assignment, quality_gates, children, mirrors, code_review_items. Absent/empty = minimal scalars only."),
|
|
290
298
|
...boardField,
|
|
291
299
|
}, async (args) => jsonResult(await issueGet(client, args)));
|
|
292
300
|
// ---------------- issue_create ----------------
|
|
293
301
|
server.tool("issue_create", 'Create a fresh card via POST /api/issues. Board-scoped; defaults to the dispatch\'s board. Pass `board` (a qualified id `<repo>:<slug>`) to create the card on another board (forwarded into body.board + ?board=; unknown board → 404). INVARIANT: type=Epic REQUIRES non-empty phase_children[] (epic-with-phases atomicity per DX-575) and the route atomically inserts the epic + every phase in ONE transaction. Non-Epic types REFUSE phase_children[] with 400. Status defaults to Review (no lifecycle timestamps stamped on create). parent_id optional. ac items take {title}; phase children inherit the new epic\'s id as parent_id. Optional list_id PLACES the card directly into a column in ONE call. **Pass EITHER a board_lists id OR the list\'s display NAME (case-insensitive, emoji-tolerant — e.g. a queue name like "⚙️ Fulfillment Queue" or just "Fulfillment Queue") — the server resolves a name to its id.** The card lands DIRECTLY in that column with the matching lifecycle stamped automatically — a `ready`-type queue → ToDo, a `completed` list → Done, etc. **You do NOT need a separate issue_transition(ready) + issue_edit(list_id) afterward — just pass the queue name here and the card is created already in that column.** Omit list_id for the default (Review). NOT valid on type=Epic (Epic status derives from children) → 400. Unknown name/id → 400. **gate_decisions is REQUIRED whenever the board has any OPTIONAL quality gate for the card\'s type** (DX-1594): supply one `{gate, enabled, note}` per board-optional gate. The create FAILS CLOSED — a missing decision returns 400 `{error, required_gate_decisions:[...]}` enumerating exactly which gates to answer, so just retry with a decision for each listed gate. `required`/`disabled` board gates take no decision; a board with no optional gates needs no gate_decisions at all. **ALWAYS pass `triage_enabled` explicitly** (root card AND every phase_children[] entry): decide per card whether it should enter the automatic triage/dispatch pipeline — `true` only when auto-triage is expected without further human review; absent → false, the card is NEVER auto-triaged (explicit-only, reverting DX-1928 — auto-created cards must never silently enter the dispatch pipeline).', {
|
|
294
302
|
type: z.enum(ISSUE_TYPES),
|
|
295
|
-
title: z.string().min(1),
|
|
296
|
-
|
|
303
|
+
title: z.string().min(1).describe(TITLE_DESCRIBE),
|
|
304
|
+
summary: z.string().min(1).optional().describe(SUMMARY_DESCRIBE),
|
|
305
|
+
description: z.string().describe(DESCRIPTION_DESCRIBE),
|
|
297
306
|
parent_id: z.string().nullable().optional(),
|
|
298
307
|
ac: z.array(z.object({ title: z.string().min(1) })).optional(),
|
|
299
308
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
@@ -310,8 +319,13 @@ server.tool("issue_create", 'Create a fresh card via POST /api/issues. Board-sco
|
|
|
310
319
|
phase_children: z
|
|
311
320
|
.array(z.object({
|
|
312
321
|
type: z.enum(NON_EPIC_TYPES),
|
|
313
|
-
title: z.string().min(1),
|
|
314
|
-
|
|
322
|
+
title: z.string().min(1).describe(TITLE_DESCRIBE),
|
|
323
|
+
summary: z
|
|
324
|
+
.string()
|
|
325
|
+
.min(1)
|
|
326
|
+
.optional()
|
|
327
|
+
.describe(`${SUMMARY_DESCRIBE} This child's OWN summary — never inherited from the root card.`),
|
|
328
|
+
description: z.string().describe(DESCRIPTION_DESCRIBE),
|
|
315
329
|
ac: z.array(z.object({ title: z.string().min(1) })).optional(),
|
|
316
330
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
317
331
|
gate_decisions: z
|
|
@@ -336,10 +350,16 @@ server.tool("issue_create", 'Create a fresh card via POST /api/issues. Board-sco
|
|
|
336
350
|
...boardField,
|
|
337
351
|
}, async (args) => jsonResult(await issueCreate(client, args, config.board)));
|
|
338
352
|
// ---------------- issue_edit ----------------
|
|
339
|
-
server.tool("issue_edit", 'Patch prose + structured fields via PATCH /api/issues/:id/edit. ALLOWED keys: title, description, ac, checklists, effort_level, parent_id, priority, list_id, triage_enabled, type. ANY OTHER KEY (lifecycle timestamps, triage state, dependencies, retro, requires_human, blocked/dispatch gates) returns 400 with offending_keys[] and a pointer to the dedicated semantic handler — use issue_transition / issue_triage / issue_comment / issue_dependency / issue_requires_human / issue_retro instead. TYPE (DX-2484): change the card\'s `type` via the `type` key — the route recomputes `dispatchable_derived` immediately when it changes. Changing type TO `Story`, `Bug`, or `Chore` makes the card eligible for autonomous pickup (subject to every other dispatch gate); changing it TO `Task` or a container (`Epic`/`Feature`) REMOVES that eligibility — a planning record or container is never dispatched, in ANY status. This is the way to promote a planning item into real work. PRIORITY (DX-1532): set card priority via the `priority` key — a tier WORD ("lowest"/"low"/"medium"/"high"/"very_high"/"critical", resolved to the tier midpoint) OR a raw number in [0,6). This is the ONLY way to change priority: the numeric `issues.priority` column is what the Trello priority label AND the dashboard badge read — editing a "Priority: <x>" line in the DESCRIPTION changes nothing downstream (a silent false-positive). To honor a "set priority" request, write `priority` here, do NOT edit description prose. CHECKLISTS (DX-1290, extended DX-2653): a card carries 0..N named checklists, each item ONE status `incomplete|failing|passing|cancelled|deferred` (terminal = passing|cancelled|deferred). `deferred` is the honest disposition for a criterion whose work is done but names a real-world/post-deploy check still outstanding — REQUIRES a non-empty `detail`; a criterion marked with the `📡` post-deploy-evidence prefix can never be `passing` at all (refused 409 at completion, resolve to `deferred` instead). `ac` is the 2-state CONVENIENCE onto the default "Acceptance Criteria" checklist (checked:true ↔ passing, false ↔ incomplete BY DEFAULT — pass `status` alongside `checked` to reach `cancelled`/`failing`/`deferred` instead) — each incoming item is DIFFED against that checklist\'s current live items (matched by the optional `check_item_id`, else by exact title) so an unchanged item keeps its id; only changed items are updated in place, new titles are inserted, and items missing from the array are removed. `checklists` is the GENERIC wholesale write path: it REPLACES every named checklist on the card with full status control (each `{name, items:[{label, detail?, status}]}`) — use it to author named checklists like "Feature Tests". Send EITHER `ac` OR `checklists`, NOT both (400). list_id (DX-1192 / DX-1200) PINS the card to a specific board list. **Pass EITHER a board_lists id OR the list\'s display NAME (case-insensitive, e.g. a queue name like "⚙️ Fulfillment Queue") — the server resolves a name to its id.** Its type MUST match the card\'s CURRENT derived-status list-type (so to route a ToDo card into a `ready`-type queue, ready it first; mismatch / unknown name or id → 400); pass null to clear the pin (back to default-for-type).', {
|
|
353
|
+
server.tool("issue_edit", 'Patch prose + structured fields via PATCH /api/issues/:id/edit. ALLOWED keys: title, summary, description, ac, checklists, effort_level, parent_id, priority, list_id, triage_enabled, type. ANY OTHER KEY (lifecycle timestamps, triage state, dependencies, retro, requires_human, blocked/dispatch gates) returns 400 with offending_keys[] and a pointer to the dedicated semantic handler — use issue_transition / issue_triage / issue_comment / issue_dependency / issue_requires_human / issue_retro instead. TYPE (DX-2484): change the card\'s `type` via the `type` key — the route recomputes `dispatchable_derived` immediately when it changes. Changing type TO `Story`, `Bug`, or `Chore` makes the card eligible for autonomous pickup (subject to every other dispatch gate); changing it TO `Task` or a container (`Epic`/`Feature`) REMOVES that eligibility — a planning record or container is never dispatched, in ANY status. This is the way to promote a planning item into real work. PRIORITY (DX-1532): set card priority via the `priority` key — a tier WORD ("lowest"/"low"/"medium"/"high"/"very_high"/"critical", resolved to the tier midpoint) OR a raw number in [0,6). This is the ONLY way to change priority: the numeric `issues.priority` column is what the Trello priority label AND the dashboard badge read — editing a "Priority: <x>" line in the DESCRIPTION changes nothing downstream (a silent false-positive). To honor a "set priority" request, write `priority` here, do NOT edit description prose. CHECKLISTS (DX-1290, extended DX-2653): a card carries 0..N named checklists, each item ONE status `incomplete|failing|passing|cancelled|deferred` (terminal = passing|cancelled|deferred). `deferred` is the honest disposition for a criterion whose work is done but names a real-world/post-deploy check still outstanding — REQUIRES a non-empty `detail`; a criterion marked with the `📡` post-deploy-evidence prefix can never be `passing` at all (refused 409 at completion, resolve to `deferred` instead). `ac` is the 2-state CONVENIENCE onto the default "Acceptance Criteria" checklist (checked:true ↔ passing, false ↔ incomplete BY DEFAULT — pass `status` alongside `checked` to reach `cancelled`/`failing`/`deferred` instead) — each incoming item is DIFFED against that checklist\'s current live items (matched by the optional `check_item_id`, else by exact title) so an unchanged item keeps its id; only changed items are updated in place, new titles are inserted, and items missing from the array are removed. `checklists` is the GENERIC wholesale write path: it REPLACES every named checklist on the card with full status control (each `{name, items:[{label, detail?, status}]}`) — use it to author named checklists like "Feature Tests". Send EITHER `ac` OR `checklists`, NOT both (400). list_id (DX-1192 / DX-1200) PINS the card to a specific board list. **Pass EITHER a board_lists id OR the list\'s display NAME (case-insensitive, e.g. a queue name like "⚙️ Fulfillment Queue") — the server resolves a name to its id.** Its type MUST match the card\'s CURRENT derived-status list-type (so to route a ToDo card into a `ready`-type queue, ready it first; mismatch / unknown name or id → 400); pass null to clear the pin (back to default-for-type).', {
|
|
340
354
|
id: z.string().min(1),
|
|
341
|
-
title: z.string().min(1).optional(),
|
|
342
|
-
|
|
355
|
+
title: z.string().min(1).optional().describe(TITLE_DESCRIBE),
|
|
356
|
+
summary: z
|
|
357
|
+
.string()
|
|
358
|
+
.min(1)
|
|
359
|
+
.nullable()
|
|
360
|
+
.optional()
|
|
361
|
+
.describe(`${SUMMARY_DESCRIBE} Pass null to clear it.`),
|
|
362
|
+
description: z.string().optional().describe(DESCRIPTION_DESCRIBE),
|
|
343
363
|
type: z
|
|
344
364
|
.enum(ISSUE_TYPES)
|
|
345
365
|
.optional()
|
|
@@ -387,7 +407,7 @@ server.tool("issue_edit", 'Patch prose + structured fields via PATCH /api/issues
|
|
|
387
407
|
...boardField,
|
|
388
408
|
}, async (args) => jsonResult(await issueEdit(client, args)));
|
|
389
409
|
// ---------------- issue_transition ----------------
|
|
390
|
-
server.tool("issue_transition", "Stamp a lifecycle transition via POST /api/issues/:id/transition. **THIS IS THE ONLY WAY TO MOVE A CARD'S LIFECYCLE STATE** — DX-835 separated card lifecycle (this tool) from dispatch finalization (`mcp__danxbot__danxbot_complete`). The worker no longer infers card moves from `danxbot_complete.status`; agents that want the card to move MUST call this tool BEFORE calling `danxbot_complete`. Actions: ready (Review→ToDo), pickup (ToDo→In Progress — server checks every dispatch gate: ready_at, blocked_at, requires_human_reason, depends_on partners terminal, conflict_on partners idle; refuses 409 with failed_gate naming the cause; pass manual:true for OPERATOR-SESSION self-pickup — stamps dispatch_kind 'manual', bypasses every card-flow gate except terminal/deleted/already-dispatched, and the worker NEVER auto-transitions the card: no orphan-heal rollback, no Epic auto-rollup — use this whenever the work happens in YOUR current session rather than a worker dispatch, DX-946), rollback_pickup, **complete** (stamps completed_at — moves card to Done; this is YOUR explicit decision, not a side effect of danxbot_complete; REFUSES 409 on Epic if any phase child non-terminal — see non_terminal_phases[]; ALSO refuses 409 with failed_gate 'quality_gate_post' + failed_post_gates[] while any required POST quality gate row != pass — DX-1177), cancel (stamps cancelled_at — terminal), **block** (requires non-empty reason — stamps blocked_at + blocked_reason + clears dispatch; USE THIS when the CARD itself cannot proceed without human intervention; distinct from env-fault dispatch failures which use `danxbot_complete({status:'failed'})`), unblock, archive (parks to Backlog, clears ready_at), reopen (terminal→active, clears completed_at/cancelled_at/archived_at). Terminal cards refuse every action except reopen. Ladder timestamps preserved — forward stamps never clear earlier ones (CLAUDE.md Core Principle 2). **DX-2282 — every path into In Progress now requires an identified claimer.** For `manual:true` pickup called from a DISPATCHED-AGENT session (this MCP tool, bearer = your dispatch token): you MUST pass `assigned_agent` set to YOUR resolved agent/profile name — omitting it, or passing the shared dispatch-token identity itself, is refused 409 `assigned_agent (required, distinguishing)`. A genuine human dashboard session may omit it (auto-resolved from the real logged-in user). Every pickup flavor (work/gate/manual) is refused 409 `assigned_agent (required)` if it would otherwise leave the card with no owner at all. **A manual pickup can now also be refused 409 `failed_gate: \"dispatch_id\"` — \"pickup refused — card claimed by another actor between read and write\" — when it loses a genuine race against a concurrent manual pickup of the same idle card.** Before this, two overlapping manual pickups of the same card could both return 200: the second write silently overwrote the first winner's claim with no error and no distinguishing status code. The claiming write is now atomic (fenced on `dispatch_id IS NULL`), so the loser gets this 409 instead of a false success — on this response, do NOT retry blindly; re-check the card's current `assigned_agent`/`dispatch_id` first, since another actor already has it.", {
|
|
410
|
+
server.tool("issue_transition", "Stamp a lifecycle transition via POST /api/issues/:id/transition. **THIS IS THE ONLY WAY TO MOVE A CARD'S LIFECYCLE STATE** — DX-835 separated card lifecycle (this tool) from dispatch finalization (`mcp__danxbot__danxbot_complete`). The worker no longer infers card moves from `danxbot_complete.status`; agents that want the card to move MUST call this tool BEFORE calling `danxbot_complete`. Actions: ready (Review→ToDo), pickup (ToDo→In Progress — server checks every dispatch gate: ready_at, blocked_at, requires_human_reason, depends_on partners terminal, conflict_on partners idle; refuses 409 with failed_gate naming the cause; pass manual:true for OPERATOR-SESSION self-pickup — stamps dispatch_kind 'manual', bypasses every card-flow gate except terminal/deleted/already-dispatched, and the worker NEVER auto-transitions the card: no orphan-heal rollback, no Epic auto-rollup — use this whenever the work happens in YOUR current session rather than a worker dispatch, DX-946), rollback_pickup, **complete** (stamps completed_at — moves card to Done; this is YOUR explicit decision, not a side effect of danxbot_complete; REFUSES 409 on Epic if any phase child non-terminal — see non_terminal_phases[]; ALSO refuses 409 with failed_gate 'quality_gate_post' + failed_post_gates[] while any required POST quality gate row != pass — DX-1177), cancel (stamps cancelled_at — terminal), **block** (requires non-empty reason — stamps blocked_at + blocked_reason + clears dispatch; USE THIS when the CARD itself cannot proceed without human intervention; distinct from env-fault dispatch failures which use `danxbot_complete({status:'failed'})`), unblock, archive (parks to Backlog, clears ready_at), reopen (terminal→active, clears completed_at/cancelled_at/archived_at). Terminal cards refuse every action except reopen. Ladder timestamps preserved — forward stamps never clear earlier ones (CLAUDE.md Core Principle 2). **DX-2282 — every path into In Progress now requires an identified claimer.** For `manual:true` pickup called from a DISPATCHED-AGENT session (this MCP tool, bearer = your dispatch token): you MUST pass `assigned_agent` set to YOUR resolved agent/profile name — omitting it, or passing the shared dispatch-token identity itself, is refused 409 `assigned_agent (required, distinguishing)`. A genuine human dashboard session may omit it (auto-resolved from the real logged-in user). Every pickup flavor (work/gate/manual) is refused 409 `assigned_agent (required)` if it would otherwise leave the card with no owner at all. **A manual pickup can now also be refused 409 `failed_gate: \"dispatch_id\"` — \"pickup refused — card claimed by another actor between read and write\" — when it loses a genuine race against a concurrent manual pickup of the same idle card.** Before this, two overlapping manual pickups of the same card could both return 200: the second write silently overwrote the first winner's claim with no error and no distinguishing status code. The claiming write is now atomic (fenced on `dispatch_id IS NULL`), so the loser gets this 409 instead of a false success — on this response, do NOT retry blindly; re-check the card's current `assigned_agent`/`dispatch_id` first, since another actor already has it. **A successful `block` returns a `solutions_reminder` beside the envelope** — `{solution_count, instruction}` — because blocking stops the card for a human: before you stop, EVERY viable solution must be listed on the card with issue_solution (the operator answers by picking one). A count of zero means you have listed none.", {
|
|
391
411
|
id: z.string().min(1),
|
|
392
412
|
action: z.enum(TRANSITION_ACTIONS),
|
|
393
413
|
reason: z.string().optional(),
|
|
@@ -442,6 +462,26 @@ server.tool("issue_checklist", "Targeted checklist CUD via /api/issues/:id/check
|
|
|
442
462
|
status: z.enum(CHECKLIST_ITEM_STATUSES).optional(),
|
|
443
463
|
...boardField,
|
|
444
464
|
}, async (args) => jsonResult(await issueChecklist(client, args)));
|
|
465
|
+
// ---------------- issue_solution ----------------
|
|
466
|
+
server.tool("issue_solution", "A card's candidate SOLUTIONS — the options the operator picks from when a card is stopped for a human decision — via /api/issues/:id/solutions[/:sid]. Whenever you block a card or set requires_human, list EVERY viable solution here first, and mark the one you recommend. Action-dispatched: list (GET) — the live solutions (each with its `id` and `content_hash`) plus every operator answer recorded so far (`decisions[]`, oldest first); add (POST {title, body?, pro?, con?, recommended?}) — append one option; edit (PATCH :sid {base_hash, title?, body?, pro?, con?, recommended?}) — change ONLY the fields you pass; remove (DELETE :sid {base_hash}) — soft-remove an option. `title` is a short name for the option, `body` the markdown detail of what it actually does, `pro` / `con` the case for and against. HASH-GUARDED: edit and remove REQUIRE `base_hash` = the `content_hash` you last read; a stale one is refused 409 `stale_solution` carrying `currentHash` + `currentSolution` — merge against that and retry, never re-send blindly. A card recommends AT MOST ONE live solution: a second `recommended: true` is refused 409 naming `recommended_solution_id` (edit that one to recommended:false first). An option the operator has already CHOSEN cannot be edited (409) — add a new solution instead; it can still be removed, and the answer keeps its title. There is no answer action: the operator answers in the dashboard.", {
|
|
467
|
+
id: z.string().min(1),
|
|
468
|
+
action: z.enum(["list", "add", "edit", "remove"]),
|
|
469
|
+
solution_id: z.number().int().positive().optional().describe("Target solution id — required for edit / remove."),
|
|
470
|
+
base_hash: z
|
|
471
|
+
.string()
|
|
472
|
+
.min(1)
|
|
473
|
+
.optional()
|
|
474
|
+
.describe("The solution's content_hash from your last read — required for edit / remove."),
|
|
475
|
+
title: z.string().min(1).optional().describe("Short name for the option — required for add."),
|
|
476
|
+
body: z.string().optional().describe("Markdown detail of what this option actually does."),
|
|
477
|
+
pro: z.string().optional().describe("The case FOR this option."),
|
|
478
|
+
con: z.string().optional().describe("The case AGAINST this option."),
|
|
479
|
+
recommended: z
|
|
480
|
+
.boolean()
|
|
481
|
+
.optional()
|
|
482
|
+
.describe("true on the single option you recommend. At most one live recommended solution per card."),
|
|
483
|
+
...boardField,
|
|
484
|
+
}, async (args) => jsonResult(await issueSolution(client, args)));
|
|
445
485
|
// ---------------- issue_dependency ----------------
|
|
446
486
|
server.tool("issue_dependency", 'Dependency CRUD via /api/issues/:id/dependencies[/:did]. action=add → POST {kind, target_id, reason} where kind ∈ {depends_on, conflict_on}. depends_on adds are CYCLE-CHECKED (BFS from target back to source — 409 if loop). Idempotent: re-adding a live triple returns the existing id. Self-loops refuse 409. action=remove → DELETE /:did. The server REQUIRES the literal reason="recorded_in_error" on removal (encodes "removal means NOT related, never satisfied") — this MCP boundary hardcodes it, so callers do not pass reason on remove. This is the ONLY mechanism the dispatch picker enforces to sequence one card after another — leaving a card at a Review/held status (e.g. an issue_triage "keep" verdict) is NOT a substitute and provides no cross-card ordering protection.', {
|
|
447
487
|
id: z.string().min(1),
|
|
@@ -453,7 +493,7 @@ server.tool("issue_dependency", 'Dependency CRUD via /api/issues/:id/dependencie
|
|
|
453
493
|
...boardField,
|
|
454
494
|
}, async (args) => jsonResult(await issueDependency(client, args)));
|
|
455
495
|
// ---------------- issue_requires_human ----------------
|
|
456
|
-
server.tool("issue_requires_human", "Set or clear the requires_human dispatch gate via /api/issues/:id/requires-human. set=true → POST {reason, steps[]} — sets requires_human_reason (the dispatch gate per DX-704 — poller refuses pickup while non-null), set_by from bearer, set_at NOW(), REPLACES the step rows (prior soft-deleted, fresh ordinals). set=false → DELETE — clears the columns and soft-deletes every live step. Terminal cards refuse 409 on set.", {
|
|
496
|
+
server.tool("issue_requires_human", "Set or clear the requires_human dispatch gate via /api/issues/:id/requires-human. set=true → POST {reason, steps[]} — sets requires_human_reason (the dispatch gate per DX-704 — poller refuses pickup while non-null), set_by from bearer, set_at NOW(), REPLACES the step rows (prior soft-deleted, fresh ordinals). set=false → DELETE — clears the columns and soft-deletes every live step. Terminal cards refuse 409 on set. **A successful set=true returns a `solutions_reminder` beside the envelope** — `{solution_count, instruction}`: before you stop, EVERY viable solution to the question you are asking must be listed on the card with issue_solution, so the operator can answer by picking one. A count of zero means you have listed none.", {
|
|
457
497
|
id: z.string().min(1),
|
|
458
498
|
set: z.boolean(),
|
|
459
499
|
reason: z.string().optional(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thehammer/danx-dashboard-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.52",
|
|
4
4
|
"description": "Stdio MCP server wrapping danxbot's dashboard /api/issues/* normalized DB-backed HTTP routes for dispatched agents (DX-704 Phase 2).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|