@thehammer/danx-dashboard-mcp 0.1.37 → 0.1.39

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
@@ -17,7 +17,7 @@ BOARD-ONLY (DX-1171): board is the first-level concept; repo is DERIVED from boa
17
17
 
18
18
  ## Tool surface
19
19
 
20
- All exposed as `mcp__danx_dashboard__<name>` once wired through the workspace `mcp.template.json`.
20
+ All exposed as `mcp__danx-dashboard__<name>` once wired through the workspace `mcp.template.json`.
21
21
 
22
22
  | Tool | HTTP | Notes |
23
23
  |---|---|---|
package/dist/handlers.js CHANGED
@@ -156,6 +156,9 @@ export async function issueEdit(client, args) {
156
156
  });
157
157
  }
158
158
  export async function issueTransition(client, args) {
159
+ if (args.action === "pickup" && args.manual && !args.assigned_agent) {
160
+ throw new Error("issue_transition({action:'pickup', manual:true}) requires assigned_agent — DX-2282: pass the resolved agent/profile name claiming this card.");
161
+ }
159
162
  const { id, board, ...body } = args;
160
163
  return client.request({
161
164
  method: "POST",
@@ -410,6 +413,53 @@ export async function issueQualityGate(client, args) {
410
413
  board: args.board,
411
414
  });
412
415
  }
416
+ /**
417
+ * Stamp an operator MANUAL quality-gate verdict via
418
+ * PATCH /api/issues/:id/quality-gates/:gate {status, message} — the same write
419
+ * the dashboard Gates-tab Pass / Fail / Revert controls perform (DX-1373).
420
+ *
421
+ * SIBLING, NOT A REPLACEMENT, of `issueQualityGate` above: they are two
422
+ * distinct operations on one resource, mirroring the server's own verb split.
423
+ * POST flips the per-card `required` FLAG (does this gate run at all); PATCH
424
+ * stamps the VERDICT (did it pass). Neither substitutes for the other, which
425
+ * is why this is a separate tool rather than an overloaded mode on the
426
+ * existing one (CLAUDE.md Core Principle 2 — no dual-shape).
427
+ *
428
+ * DX-2222 — why this tool has to exist. A card cannot reach Done while any
429
+ * required POST gate row is not `pass` (`issue_transition complete` refuses
430
+ * 409 with `failed_gate: "quality_gate_post"`). Until now the ONLY way to
431
+ * write a verdict was the worker route `src/worker/quality-gate-route.ts`,
432
+ * authed by the per-dispatch `DANX_AGENT_TOKEN` that exists only INSIDE a
433
+ * spawned dispatch. An operator/host session (`issue_transition pickup
434
+ * {manual:true}`, DX-946) has no such token, so it could pick a card up and
435
+ * do the work but never close it — every manually-claimed card stranded In
436
+ * Progress, and each stranded card then projects `conflict_on` / `waiting_on`
437
+ * gates onto the ToDo queue, stalling dispatch for cards nobody is working.
438
+ *
439
+ * This route is the RIGHT one to expose for that: its handler
440
+ * (`src/issues/write/quality-gate-verdict.ts`) is a pure, dispatch-less row
441
+ * write authed by the ordinary per-user dashboard bearer — explicitly "NOT
442
+ * the dispatch token — there is no dispatch behind a manual stamp". It does
443
+ * NOT fire the worker route's per-dispatch side effects (`gate_release` on
444
+ * pass, `block` on fail), so a manual verdict can never release or block a
445
+ * dispatch that does not exist.
446
+ *
447
+ * Fail-loud (CP1), all server-side: unknown `gate` → 400; `status` outside
448
+ * pass/fail/pending → 400; a pass/fail whose `message` is under 20 characters
449
+ * → 400, because the note IS the accountability record for a human override.
450
+ * `pending` reverts a prior verdict and clears the message.
451
+ */
452
+ export async function issueQualityGateVerdict(client, args) {
453
+ const body = { status: args.status };
454
+ if (args.message !== undefined)
455
+ body.message = args.message;
456
+ return client.request({
457
+ method: "PATCH",
458
+ path: `/${encodeURIComponent(args.id)}/quality-gates/${encodeURIComponent(args.gate)}`,
459
+ body,
460
+ board: args.board,
461
+ });
462
+ }
413
463
  export async function issueRetro(client, args) {
414
464
  const { id, board, ...body } = args;
415
465
  return client.request({
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * tool here POSTs/GETs against the dashboard, which atomically applies
9
9
  * the change via the transactional layer and publishes SSE.
10
10
  *
11
- * Tool surface (all exposed as `mcp__danx_dashboard__<name>` once wired
11
+ * Tool surface (all exposed as `mcp__danx-dashboard__<name>` once wired
12
12
  * through the workspace `mcp.template.json`):
13
13
  *
14
14
  * - issue_list GET /api/issues
@@ -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];
@@ -305,13 +307,18 @@ server.tool("issue_edit", 'Patch prose + structured fields via PATCH /api/issues
305
307
  ...boardField,
306
308
  }, async (args) => jsonResult(await issueEdit(client, args)));
307
309
  // ---------------- issue_transition ----------------
308
- 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).", {
310
+ 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 — `assigned_agent` is REQUIRED on every `manual:true` pickup call from a dispatched agent**: the server resolves your MCP-tool bearer to the single synthetic identity shared by EVERY dispatch, which cannot stand in as 'who claimed this' — pass the resolved agent/profile name explicitly (never omit it, never pass the generic dispatch identity itself) or the call is refused 409.", {
309
311
  id: z.string().min(1),
310
312
  action: z.enum(TRANSITION_ACTIONS),
311
313
  reason: z.string().optional(),
312
314
  summary: z.string().optional(),
313
315
  dispatch_id: z.string().min(1).optional(),
314
316
  manual: z.boolean().optional(),
317
+ assigned_agent: z
318
+ .string()
319
+ .min(1)
320
+ .optional()
321
+ .describe("DX-2282 — REQUIRED when manual:true (operator/dispatched-agent self-pickup): the resolved agent/profile name claiming this card. Must be an explicit, distinguishing identity — never omitted, never the generic shared dispatch-token identity — or the pickup is refused 409."),
315
322
  ...boardField,
316
323
  }, async (args) => jsonResult(await issueTransition(client, args)));
317
324
  // ---------------- issue_triage ----------------
@@ -388,6 +395,21 @@ server.tool("issue_quality_gate", "Toggle a single card's per-card quality-gate
388
395
  effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
389
396
  ...boardField,
390
397
  }, async (args) => jsonResult(await issueQualityGate(client, args)));
398
+ // ---------------- issue_quality_gate_verdict ----------------
399
+ 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.", {
400
+ id: z.string().min(1),
401
+ gate: z.enum([
402
+ "plan-dependency",
403
+ "plan-architecture",
404
+ "plan-tdd",
405
+ "code-test-quality",
406
+ "code-architecture",
407
+ "code-quality",
408
+ ]),
409
+ status: z.enum(["pass", "fail", "pending"]),
410
+ message: z.string().optional(),
411
+ ...boardField,
412
+ }, async (args) => jsonResult(await issueQualityGateVerdict(client, args)));
391
413
  // ---------------- issue_retro ----------------
392
414
  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
415
  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.39",
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",