@thehammer/danx-dashboard-mcp 0.1.34 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/handlers.js CHANGED
@@ -120,6 +120,11 @@ export async function issueCreate(client, args, defaultBoard) {
120
120
  body.list_id = args.list_id;
121
121
  if (args.gate_decisions !== undefined)
122
122
  body.gate_decisions = args.gate_decisions;
123
+ // DX-1895 (explicit-only) — forward the caller's explicit auto-triage
124
+ // decision; the server stamps false when absent (never auto-triaged).
125
+ // Per-child flags ride along inside phase_children entries verbatim.
126
+ if (args.triage_enabled !== undefined)
127
+ body.triage_enabled = args.triage_enabled;
123
128
  if (args.phase_children !== undefined)
124
129
  body.phase_children = args.phase_children;
125
130
  return client.request({ method: "POST", path: "", body, board });
@@ -178,7 +183,7 @@ export async function issueComment(client, args) {
178
183
  return client.request({
179
184
  method: "POST",
180
185
  path: `/${idEnc}/comments`,
181
- body: { text: args.text },
186
+ body: { text: args.text, ...(args.metadata !== undefined ? { metadata: args.metadata } : {}) },
182
187
  board,
183
188
  });
184
189
  }
package/dist/index.js CHANGED
@@ -225,7 +225,7 @@ server.tool("issue_get", "Fetch a single issue via GET /api/issues/:id. Board-sc
225
225
  ...boardField,
226
226
  }, async (args) => jsonResult(await issueGet(client, args)));
227
227
  // ---------------- issue_create ----------------
228
- 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.', {
228
+ 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).', {
229
229
  type: z.enum(ISSUE_TYPES),
230
230
  title: z.string().min(1),
231
231
  description: z.string(),
@@ -258,8 +258,16 @@ server.tool("issue_create", 'Create a fresh card via POST /api/issues. Board-sco
258
258
  }))
259
259
  .optional()
260
260
  .describe("Per-child fail-closed gate decisions — same shape + rule as the root gate_decisions (incl. the optional per-gate effort_level, DX-1760), resolved against THIS child's own type. Required when the child's type has board-optional gates."),
261
+ triage_enabled: z
262
+ .boolean()
263
+ .optional()
264
+ .describe("Per-child auto-triage opt-in (explicit-only, reverting DX-1928). ALWAYS pass explicitly per child — decide whether THIS child should enter the automatic triage pipeline. Absent → false (never auto-triaged). Never inherited from the root card."),
261
265
  }))
262
266
  .optional(),
267
+ triage_enabled: z
268
+ .boolean()
269
+ .optional()
270
+ .describe("DX-1895 (explicit-only — reverting DX-1928): per-card opt-in for the automatic triage dispatcher. ALWAYS pass this explicitly — decide per card whether auto-triage is expected. true = the card enters the automatic triage/dispatch pipeline without further human review; absent → false, the card is NEVER auto-triaged. Operator-directed POST /api/triage and direct issue_triage calls are NOT gated by this flag."),
263
271
  ...boardField,
264
272
  }, async (args) => jsonResult(await issueCreate(client, args, config.board)));
265
273
  // ---------------- issue_edit ----------------
@@ -293,7 +301,7 @@ server.tool("issue_edit", 'Patch prose + structured fields via PATCH /api/issues
293
301
  triage_enabled: z
294
302
  .boolean()
295
303
  .optional()
296
- .describe("DX-1895 — per-card opt-in for the DX-1886 auto-triage dispatcher. false (default) = the automatic dispatcher trigger never selects this card, even when every other eligibility condition holds. Operator-directed POST /api/triage and direct issue_triage calls are NOT gated by this flag."),
304
+ .describe("DX-1895 (explicit-only reverting DX-1928): per-card opt-in for the DX-1886 auto-triage dispatcher. Default false the create path stamps false unless the creator explicitly set it, so this edit field is the only post-create way to opt a card in or out. false = the automatic dispatcher trigger never selects this card, even when every other eligibility condition holds. This flag gates ONLY the automatic dispatcher; operator-directed POST /api/triage and direct issue_triage calls remain flag-independent."),
297
305
  ...boardField,
298
306
  }, async (args) => jsonResult(await issueEdit(client, args)));
299
307
  // ---------------- issue_transition ----------------
@@ -314,11 +322,12 @@ server.tool("issue_triage", "Record a triage confidence score via POST /api/issu
314
322
  ...boardField,
315
323
  }, async (args) => jsonResult(await issueTriage(client, args)));
316
324
  // ---------------- issue_comment ----------------
317
- server.tool("issue_comment", "Comment CRUD via /api/issues/:id/comments[/:cid]. action=add → POST {text} (server stamps author from bearer + auto-incrementing ordinal); action=edit → PATCH /:cid {text}; action=delete → DELETE /:cid (soft-delete, audit trail preserved — comments are NEVER hard-deleted). Client-supplied author is IGNORED (server-stamped to prevent impersonation).", {
325
+ server.tool("issue_comment", "Comment CRUD via /api/issues/:id/comments[/:cid]. action=add → POST {text, metadata?} (server stamps author from bearer + auto-incrementing ordinal); action=edit → PATCH /:cid {text}; action=delete → DELETE /:cid (soft-delete, audit trail preserved — comments are NEVER hard-deleted). Client-supplied author is IGNORED (server-stamped to prevent impersonation). `metadata` (DX-2157, action=add only) is an OPTIONAL opaque JSON object a calling app attaches to the comment — e.g. a generated `{sql, explanation}` packet its own UI renders specially. danxbot stores + returns it verbatim and enforces NO shape on its contents; omit for a plain markdown-only comment (unaffected either way).", {
318
326
  id: z.string().min(1),
319
327
  action: z.enum(["add", "edit", "delete"]),
320
328
  comment_id: z.number().int().positive().optional(),
321
329
  text: z.string().min(1).optional(),
330
+ metadata: z.record(z.unknown()).optional(),
322
331
  ...boardField,
323
332
  }, async (args) => jsonResult(await issueComment(client, args)));
324
333
  // ---------------- issue_checklist ----------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
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",
@@ -1,51 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { DashboardHttpClient } from "./http-client.js";
3
- import { issueList, issueTransition } from "./handlers.js";
4
- /**
5
- * `issue_list` query-param forwarding. A fake `fetch` captures the built
6
- * URL so each filter is asserted at the wire, through the real client's
7
- * URL builder (`?repo=` always stamped, extra query merged after).
8
- */
9
- function clientCapturing() {
10
- const urls = [];
11
- const fetchImpl = (async (url) => {
12
- urls.push(url);
13
- return new Response(JSON.stringify({ issues: [] }), { status: 200 });
14
- });
15
- const client = new DashboardHttpClient({ baseUrl: "http://localhost:5555", repo: "danxbot", token: "t" }, fetchImpl);
16
- return { client, urls };
17
- }
18
- describe("issueList — query forwarding", () => {
19
- it("forwards q as the server-side search needle", async () => {
20
- const { client, urls } = clientCapturing();
21
- await issueList(client, { q: "retire" });
22
- expect(urls[0]).toContain("q=retire");
23
- });
24
- it("omits q when not provided", async () => {
25
- const { client, urls } = clientCapturing();
26
- await issueList(client, { include_closed: true });
27
- expect(urls[0]).not.toContain("q=");
28
- expect(urls[0]).toContain("include_closed=true");
29
- });
30
- });
31
- describe("issueTransition — body forwarding", () => {
32
- function clientCapturingBody() {
33
- const bodies = [];
34
- const fetchImpl = (async (_url, init) => {
35
- bodies.push(JSON.parse(String(init?.body ?? "{}")));
36
- return new Response(JSON.stringify({ issue: {} }), { status: 200 });
37
- });
38
- const client = new DashboardHttpClient({ baseUrl: "http://localhost:5555", repo: "danxbot", token: "t" }, fetchImpl);
39
- return { client, bodies };
40
- }
41
- it("forwards manual: true on pickup (DX-946 operator self-pickup)", async () => {
42
- const { client, bodies } = clientCapturingBody();
43
- await issueTransition(client, { id: "DX-1", action: "pickup", manual: true });
44
- expect(bodies[0]).toEqual({ action: "pickup", manual: true });
45
- });
46
- it("omits manual when not provided", async () => {
47
- const { client, bodies } = clientCapturingBody();
48
- await issueTransition(client, { id: "DX-1", action: "pickup" });
49
- expect(bodies[0]).toEqual({ action: "pickup" });
50
- });
51
- });