@thehammer/danx-dashboard-mcp 0.1.36 → 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 +47 -0
- package/dist/handlers.test.js +51 -0
- package/dist/index.js +18 -1
- package/package.json +1 -1
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.
|
|
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",
|