@thehammer/danx-dashboard-mcp 0.1.37 → 0.1.38

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
@@ -410,6 +410,53 @@ export async function issueQualityGate(client, args) {
410
410
  board: args.board,
411
411
  });
412
412
  }
413
+ /**
414
+ * Stamp an operator MANUAL quality-gate verdict via
415
+ * PATCH /api/issues/:id/quality-gates/:gate {status, message} — the same write
416
+ * the dashboard Gates-tab Pass / Fail / Revert controls perform (DX-1373).
417
+ *
418
+ * SIBLING, NOT A REPLACEMENT, of `issueQualityGate` above: they are two
419
+ * distinct operations on one resource, mirroring the server's own verb split.
420
+ * POST flips the per-card `required` FLAG (does this gate run at all); PATCH
421
+ * stamps the VERDICT (did it pass). Neither substitutes for the other, which
422
+ * is why this is a separate tool rather than an overloaded mode on the
423
+ * existing one (CLAUDE.md Core Principle 2 — no dual-shape).
424
+ *
425
+ * DX-2222 — why this tool has to exist. A card cannot reach Done while any
426
+ * required POST gate row is not `pass` (`issue_transition complete` refuses
427
+ * 409 with `failed_gate: "quality_gate_post"`). Until now the ONLY way to
428
+ * write a verdict was the worker route `src/worker/quality-gate-route.ts`,
429
+ * authed by the per-dispatch `DANX_AGENT_TOKEN` that exists only INSIDE a
430
+ * spawned dispatch. An operator/host session (`issue_transition pickup
431
+ * {manual:true}`, DX-946) has no such token, so it could pick a card up and
432
+ * do the work but never close it — every manually-claimed card stranded In
433
+ * Progress, and each stranded card then projects `conflict_on` / `waiting_on`
434
+ * gates onto the ToDo queue, stalling dispatch for cards nobody is working.
435
+ *
436
+ * This route is the RIGHT one to expose for that: its handler
437
+ * (`src/issues/write/quality-gate-verdict.ts`) is a pure, dispatch-less row
438
+ * write authed by the ordinary per-user dashboard bearer — explicitly "NOT
439
+ * the dispatch token — there is no dispatch behind a manual stamp". It does
440
+ * NOT fire the worker route's per-dispatch side effects (`gate_release` on
441
+ * pass, `block` on fail), so a manual verdict can never release or block a
442
+ * dispatch that does not exist.
443
+ *
444
+ * Fail-loud (CP1), all server-side: unknown `gate` → 400; `status` outside
445
+ * pass/fail/pending → 400; a pass/fail whose `message` is under 20 characters
446
+ * → 400, because the note IS the accountability record for a human override.
447
+ * `pending` reverts a prior verdict and clears the message.
448
+ */
449
+ export async function issueQualityGateVerdict(client, args) {
450
+ const body = { status: args.status };
451
+ if (args.message !== undefined)
452
+ body.message = args.message;
453
+ return client.request({
454
+ method: "PATCH",
455
+ path: `/${encodeURIComponent(args.id)}/quality-gates/${encodeURIComponent(args.gate)}`,
456
+ body,
457
+ board: args.board,
458
+ });
459
+ }
413
460
  export async function issueRetro(client, args) {
414
461
  const { id, board, ...body } = args;
415
462
  return client.request({
@@ -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
@@ -22,6 +22,8 @@
22
22
  * - issue_dependency POST/DELETE /api/issues/:id/dependencies[/:did]
23
23
  * - issue_requires_human POST/DELETE /api/issues/:id/requires-human
24
24
  * - issue_quality_gate POST /api/issues/:id/quality-gates/:gate
25
+ * - issue_quality_gate_verdict
26
+ * PATCH /api/issues/:id/quality-gates/:gate
25
27
  * - issue_retro PUT /api/issues/:id/retro
26
28
  * - issue_attach POST /api/issues/:id/attachments (reads a local file)
27
29
  * - repo_knowledge_get GET /api/repo-knowledge
@@ -55,7 +57,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
55
57
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
56
58
  import { z } from "zod";
57
59
  import { DashboardHttpClient } from "./http-client.js";
58
- import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueQualityGate, issueRequiresHuman, issueRetro, issueTransition, issueTriage, repoKnowledgeGet, repoKnowledgeSet, } from "./handlers.js";
60
+ import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueQualityGate, issueQualityGateVerdict, issueRequiresHuman, issueRetro, issueTransition, issueTriage, repoKnowledgeGet, repoKnowledgeSet, } from "./handlers.js";
59
61
  import { PRIORITY_TIER_WORDS } from "./priority.js";
60
62
  function readEnvOrDie(name) {
61
63
  const v = process.env[name];
@@ -388,6 +390,21 @@ server.tool("issue_quality_gate", "Toggle a single card's per-card quality-gate
388
390
  effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
389
391
  ...boardField,
390
392
  }, async (args) => jsonResult(await issueQualityGate(client, args)));
393
+ // ---------------- issue_quality_gate_verdict ----------------
394
+ server.tool("issue_quality_gate_verdict", "Stamp an operator MANUAL quality-gate VERDICT via PATCH /api/issues/:id/quality-gates/:gate {status, message} — the same write the dashboard Gates-tab Pass / Fail / Revert controls perform (DX-1373). SIBLING of `issue_quality_gate`, not a replacement: that one flips the per-card `required` FLAG (does this gate run at all), THIS one records the VERDICT (did it pass) — the server exposes them as POST vs PATCH on the same resource and neither substitutes for the other. **Use this to close out a card you picked up with `issue_transition pickup {manual:true}`** (DX-946 operator-session self-pickup): `issue_transition complete` REFUSES 409 (`failed_gate: \"quality_gate_post\"`, `failed_post_gates[]`) while any required POST gate (`code-quality` / `code-test-quality` / `code-architecture`) is not `pass`, so without a verdict a manually-claimed card can never reach Done — it strands In Progress and its `conflict_on` / `waiting_on` edges then stall OTHER cards' dispatch. `status`: `pass` | `fail` | `pending` (revert a prior verdict, clears the message). `message` is the accountability record for the override and is REQUIRED at >= 20 characters for `pass`/`fail` (shorter → 400); it is ignored for `pending`. Record the REAL reviewer finding here, not a rubber stamp — this is a human-attributed override, stamped with the operator actor, and it is what a later reader sees instead of a reviewer dispatch. A manual verdict is a PURE row write: unlike the worker's in-dispatch gate route it fires NO side effects — a manual `fail` never blocks the card and a manual `pass` never releases a dispatch. Unknown gate → 400; status outside the three values → 400; unknown card → 404. Board-scoped; pass `board` (`<repo>:<slug>`) to target another board.", {
395
+ id: z.string().min(1),
396
+ gate: z.enum([
397
+ "plan-dependency",
398
+ "plan-architecture",
399
+ "plan-tdd",
400
+ "code-test-quality",
401
+ "code-architecture",
402
+ "code-quality",
403
+ ]),
404
+ status: z.enum(["pass", "fail", "pending"]),
405
+ message: z.string().optional(),
406
+ ...boardField,
407
+ }, async (args) => jsonResult(await issueQualityGateVerdict(client, args)));
391
408
  // ---------------- issue_retro ----------------
392
409
  server.tool("issue_retro", "Replace the retro block via PUT /api/issues/:id/retro. Body: {good, bad, action_item_ids[], commits[], tests[]}. REFUSES 409 unless the card is terminal (completed_at OR cancelled_at) — retro ships when work concludes. Replace semantics: good/bad upsert; action_item_ids[] + commits[] + tests[] soft-delete prior live rows and insert with fresh ordinals. action_item_ids[] entries MUST match <PREFIX>-N. commits[] entries take {sha, subject?}. tests[] (DX-1646) is REQUIRED (empty array allowed — the \"ran no tests\" case): one row per test GROUP that ran (a whole suite/class — name the group, do NOT list individual unit tests) or per individual e2e test (kind:'e2e', listed explicitly since they are few + expensive). Each row: {name, kind:'group'|'e2e', num_tests, num_passing_tests, duration_ms} required; num_assertions + num_passing_assertions NULLABLE (vitest surfaces no assertion totals — pass null or omit).", {
393
410
  id: z.string().min(1),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
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",