@thehammer/danx-dashboard-mcp 0.1.32 → 0.1.34

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 CHANGED
@@ -26,9 +26,9 @@ All exposed as `mcp__danx_dashboard__<name>` once wired through the workspace `m
26
26
  | `issue_create` | `POST /api/issues` | Epic REQUIRES non-empty `phase_children[]` (atomic insert) |
27
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
28
  | `issue_transition` | `POST /api/issues/:id/transition` | Actions: ready, pickup, rollback_pickup, complete, cancel, block, unblock, archive, reopen |
29
- | `issue_triage` | `POST /api/issues/:id/triage` | Verdicts: approve, cancel, keep, defer (with optional ICE + ttl_seconds) |
29
+ | `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
30
  | `issue_comment` | `POST/PATCH/DELETE /api/issues/:id/comments[/:cid]` | Author server-stamped, soft-delete preserved |
31
- | `issue_dependency` | `POST/DELETE /api/issues/:id/dependencies[/:did]` | `depends_on` cycle-checked; remove hardcodes `reason: "recorded_in_error"` |
31
+ | `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
32
  | `issue_requires_human` | `POST/DELETE /api/issues/:id/requires-human` | Set replaces step rows atomically; clear soft-deletes them |
33
33
  | `issue_retro` | `PUT /api/issues/:id/retro` | Requires terminal card; replace semantics |
34
34
 
@@ -0,0 +1,51 @@
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
+ });
package/dist/index.js CHANGED
@@ -175,7 +175,6 @@ const TRANSITION_ACTIONS = [
175
175
  "archive",
176
176
  "reopen",
177
177
  ];
178
- const TRIAGE_VERDICTS = ["approve", "cancel", "keep", "defer"];
179
178
  // Optional per-call board override shared by every tool. Omitted → the
180
179
  // dispatch's env-derived board (`<repo>:<slug>`) is used; provided → that
181
180
  // board is targeted instead (the dashboard owns board→repo and 404s an
@@ -308,18 +307,10 @@ server.tool("issue_transition", "Stamp a lifecycle transition via POST /api/issu
308
307
  ...boardField,
309
308
  }, async (args) => jsonResult(await issueTransition(client, args)));
310
309
  // ---------------- issue_triage ----------------
311
- server.tool("issue_triage", "Record a triage verdict via POST /api/issues/:id/triage. Verdicts: approve (stamps ready_atmoves to ToDo, clears triage TTL), cancel (stamps cancelled_at terminal), keep (refreshes triage_expires_at by ttl_seconds defaults 7 days, card stays at Review), defer (stamps archived_at, clears ready_at parks to Backlog). ICE components optional but recorded when present (total = i+c+e). Reason is REQUIRED non-empty. REFUSES 409 on terminal cards.", {
310
+ server.tool("issue_triage", "Record a triage confidence score via POST /api/issues/:id/triage (DX-2086). Caller sends a single `confidence` integer 0-5 plus a required non-empty `reason` the server computes the verdict by comparing `confidence` against the board's configured thresholds (all band edges inclusive on the low side): confidence <= cancelThreshold -> cancel (stamps cancelled_at, terminal); cancelThreshold < confidence <= archiveThreshold -> defer (stamps archived_at AND blocked_at/blocked_reason); archiveThreshold < confidence <= reviewThreshold -> keep (stamps blocked_at/blocked_reason, stays derived-Review); confidence > reviewThreshold -> approve (stamps ready_at). REFUSES 409 on terminal cards. A keep/defer verdict now BLOCKS the card (blocked_at set) it is not a cross-card ordering gate; use issue_dependency (kind: depends_on) to sequence one card after another.", {
312
311
  id: z.string().min(1),
313
- verdict: z.enum(TRIAGE_VERDICTS),
312
+ confidence: z.number().int().min(0).max(5),
314
313
  reason: z.string().min(1),
315
- ice: z
316
- .object({
317
- i: z.number().finite(),
318
- c: z.number().finite(),
319
- e: z.number().finite(),
320
- })
321
- .optional(),
322
- ttl_seconds: z.number().positive().optional(),
323
314
  ...boardField,
324
315
  }, async (args) => jsonResult(await issueTriage(client, args)));
325
316
  // ---------------- issue_comment ----------------
@@ -356,7 +347,7 @@ server.tool("issue_checklist", "Targeted checklist CUD via /api/issues/:id/check
356
347
  ...boardField,
357
348
  }, async (args) => jsonResult(await issueChecklist(client, args)));
358
349
  // ---------------- issue_dependency ----------------
359
- 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.', {
350
+ 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.', {
360
351
  id: z.string().min(1),
361
352
  action: z.enum(["add", "remove"]),
362
353
  kind: z.enum(["depends_on", "conflict_on"]).optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
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",