@thehammer/danx-dashboard-mcp 0.1.72 → 0.1.73

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
@@ -31,6 +31,7 @@ All exposed as `mcp__danx-dashboard__<name>` once wired through the workspace `m
31
31
  | `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 |
32
32
  | `issue_comment` | `POST/PATCH/DELETE /api/issues/:id/comments[/:cid]` | Author server-stamped, soft-delete preserved |
33
33
  | `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 |
34
+ | `issue_retire_branch` | `POST /api/issues/:id/card-branch-retire` | Mark this card's own `card/<id>` branch RETIRED (unsafe to merge) — `{reason}`, `by` server-stamped. NO status/terminal gate (DX-2845): settable the instant an audit/split judges the branch unsafe, whatever the card's status. The dispatched worker reads this at its next bootstrap and forces `origin/main` instead of re-attaching to the retired content |
34
35
  | `issue_retro` | `PUT /api/issues/:id/retro` | Requires terminal card; replace semantics |
35
36
 
36
37
  ## `plan_get` — cheap by default, opt-in for the rest (DX-2727)
package/dist/handlers.js CHANGED
@@ -445,6 +445,24 @@ export async function issueDependency(client, args) {
445
445
  board,
446
446
  });
447
447
  }
448
+ /**
449
+ * DX-2845 — mark a card's own `card/<id>` branch RETIRED (unsafe to merge)
450
+ * via POST /api/issues/:id/card-branch-retire {reason}. NO status/terminal
451
+ * gate on the server side — settable the moment a branch is judged unsafe,
452
+ * whether the card is ToDo, In Progress, or anything else (see
453
+ * `retireCardBranch`'s doc comment, `src/issues/write/card-branch-retire.ts`).
454
+ * `by` is server-stamped from the resolved writing identity, never
455
+ * client-supplied. Idempotent: retiring an already-retired branch just
456
+ * re-stamps reason/actor/timestamp.
457
+ */
458
+ export async function issueRetireBranch(client, args) {
459
+ return client.request({
460
+ method: "POST",
461
+ path: `/${encodeURIComponent(args.id)}/card-branch-retire`,
462
+ body: { reason: args.reason },
463
+ board: args.board,
464
+ });
465
+ }
448
466
  /**
449
467
  * DX-1362 — targeted checklist CUD. Action-dispatched onto the
450
468
  * `/api/issues/:id/checklists[/:cid[/items[/:iid]]]` route family so an agent
@@ -1129,75 +1147,3 @@ export async function planDeleteRecord(client, args) {
1129
1147
  body: { content_hash: args.content_hash },
1130
1148
  });
1131
1149
  }
1132
- // ---------------- failure_category_list / _create / _update (DX-2792) ----------------
1133
- /**
1134
- * DX-2792 (Failure evaluation 3/4) — wraps `src/dashboard/failure-categories-routes.ts`,
1135
- * the DX-2791 (Failure evaluation 2/4) category registry's REST API. INSTALL-
1136
- * GLOBAL, not board-scoped: `failure_categories` carries no `board_id`
1137
- * column (every category applies across the whole install), so — unlike
1138
- * every `/api/issues/*`-backed tool above — these three never send a
1139
- * `board` query param and the `board` override field is simply absent from
1140
- * their schemas (mirrors the `plan_*` family's own board-less rationale in
1141
- * `index.ts`'s `boardField` comment, for the same underlying reason: nothing
1142
- * server-side would read it).
1143
- */
1144
- const FAILURE_CATEGORIES_BASE_PATH = "/api/failure-categories";
1145
- /**
1146
- * List every failure category with its live matched-occurrence count and
1147
- * last-seen time, via `GET /api/failure-categories`. Returns
1148
- * `{categories: [{id, name, description, matchers, ignore, ignoreReason,
1149
- * expectedRate, matchedCount, lastSeenMs, ...}]}`. A fresh install returns
1150
- * `{categories: []}`.
1151
- */
1152
- export async function failureCategoryList(client) {
1153
- return client.request({
1154
- method: "GET",
1155
- path: "",
1156
- basePath: FAILURE_CATEGORIES_BASE_PATH,
1157
- });
1158
- }
1159
- /**
1160
- * Create a new failure category via `POST /api/failure-categories`. At
1161
- * least one matcher, each with at least one of `sourceKind`/`tool`/
1162
- * `regexPattern` set, is required (400 otherwise). `ignore: true` REQUIRES a
1163
- * non-empty `ignoreReason` (400 otherwise). A matcher set that would overlap
1164
- * an EXISTING category's matchers is refused 400 naming the conflicting
1165
- * category — expand that category instead of creating a near-duplicate. On
1166
- * success, the dashboard re-matches every existing uncategorized occurrence
1167
- * against the new category before responding, so `matchedCount` in the
1168
- * response already reflects any newly-covered signatures.
1169
- */
1170
- export async function failureCategoryCreate(client, args) {
1171
- return client.request({
1172
- method: "POST",
1173
- path: "",
1174
- basePath: FAILURE_CATEGORIES_BASE_PATH,
1175
- body: {
1176
- name: args.name,
1177
- description: args.description ?? "",
1178
- matchers: args.matchers,
1179
- ignore: args.ignore ?? false,
1180
- ignoreReason: args.ignoreReason ?? null,
1181
- expectedRate: args.expectedRate ?? null,
1182
- },
1183
- });
1184
- }
1185
- /**
1186
- * Patch an existing failure category via `PATCH /api/failure-categories/:id`
1187
- * — the tool for BOTH "expand an existing category's matchers" (send the
1188
- * full replacement `matchers` array) and "mark a category ignored" (send
1189
- * `ignore: true` + a non-empty `ignoreReason`). At least one field is
1190
- * required (400 otherwise). Same overlap refusal as create (excluding this
1191
- * category's own prior matchers). On success, re-matches every
1192
- * uncategorized occurrence against the updated matcher set before
1193
- * responding.
1194
- */
1195
- export async function failureCategoryUpdate(client, args) {
1196
- const { id, ...patch } = args;
1197
- return client.request({
1198
- method: "PATCH",
1199
- path: `/${id}`,
1200
- basePath: FAILURE_CATEGORIES_BASE_PATH,
1201
- body: patch,
1202
- });
1203
- }
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@
22
22
  * - issue_problem GET/POST/PATCH/DELETE /api/issues/:id/problems[/:pid] (DX-2735)
23
23
  * - issue_solution POST/PATCH/DELETE /api/issues/:id/problems/:pid/solutions[/:sid] (DX-2735)
24
24
  * - issue_dependency POST/DELETE /api/issues/:id/dependencies[/:did]
25
+ * - issue_retire_branch POST /api/issues/:id/card-branch-retire (DX-2845)
25
26
  * - issue_quality_gate POST /api/issues/:id/quality-gates/:gate
26
27
  * - issue_quality_gate_verdict
27
28
  * PATCH /api/issues/:id/quality-gates/:gate
@@ -48,9 +49,6 @@
48
49
  * - plan_update_architecture_section PATCH /api/plans/mine/architecture/sections/:sid (DX-2726)
49
50
  * - plan_delete_architecture_section DELETE /api/plans/mine/architecture/sections/:sid (DX-2726)
50
51
  * - plan_reorder_architecture_section PUT /api/plans/mine/architecture/sections/reorder (DX-2726)
51
- * - failure_category_list GET /api/failure-categories (DX-2791/DX-2792, board-less)
52
- * - failure_category_create POST /api/failure-categories (DX-2791/DX-2792, board-less)
53
- * - failure_category_update PATCH /api/failure-categories/:id (DX-2791/DX-2792, board-less)
54
52
  *
55
53
  * DX-2683 — THE PLAN TOOLS ARE SESSION-BOUND, and asymmetrically so. Reads
56
54
  * may name any plan; WRITES take no plan id at all and act on the plan this
@@ -89,7 +87,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
89
87
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
90
88
  import { z } from "zod";
91
89
  import { DashboardHttpClient } from "./http-client.js";
92
- import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueProblem, issueQualityGate, issueQualityGateVerdict, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, failureCategoryCreate, failureCategoryList, failureCategoryUpdate, planAddArchitectureSection, planAddCard, planAddRecord, planConnect, planCreate, planDeleteArchitectureSection, planDeleteRecord, planGet, PLAN_FIELD_GROUPS, PLAN_STATUSES, ISSUE_BATCH_GET_MAX, LIST_PAGE_MAX_LIMIT, PLAN_GET_CARDS_DEFAULT_LIMIT, planGetArchitectureSection, planGetRecord, planList, planRemoveCard, planRename, planReorderArchitectureSection, planUpdateArchitectureSection, planUpdateRecord, repoKnowledgeGet, repoKnowledgeSet, } from "./handlers.js";
90
+ import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueProblem, issueQualityGate, issueQualityGateVerdict, issueRetireBranch, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, planAddArchitectureSection, planAddCard, planAddRecord, planConnect, planCreate, planDeleteArchitectureSection, planDeleteRecord, planGet, PLAN_FIELD_GROUPS, PLAN_STATUSES, ISSUE_BATCH_GET_MAX, LIST_PAGE_MAX_LIMIT, PLAN_GET_CARDS_DEFAULT_LIMIT, planGetArchitectureSection, planGetRecord, planList, planRemoveCard, planRename, planReorderArchitectureSection, planUpdateArchitectureSection, planUpdateRecord, repoKnowledgeGet, repoKnowledgeSet, } from "./handlers.js";
93
91
  import { PRIORITY_TIER_WORDS } from "./priority.js";
94
92
  function readEnvOrDie(name) {
95
93
  const v = process.env[name];
@@ -539,6 +537,12 @@ server.tool("issue_dependency", 'Dependency CRUD via /api/issues/:id/dependencie
539
537
  dependency_id: z.number().int().positive().optional(),
540
538
  ...boardField,
541
539
  }, async (args) => jsonResult(await issueDependency(client, args)));
540
+ // ---------------- issue_retire_branch ----------------
541
+ server.tool("issue_retire_branch", "Mark a card's own `card/<id>` branch RETIRED (unsafe to merge) via POST /api/issues/:id/card-branch-retire {reason} (DX-2845). NO status/terminal gate — settable the moment a branch is judged unsafe (an audit rejected it, the card was split into fresh slices, ...), whether the card is ToDo, In Progress, or anything else; this is deliberately NOT the same as captureAndDeleteCardBranch, which only fires once the card itself reaches Done/Cancelled. `by` is server-stamped from the resolved writing identity, never client-supplied. The dispatched worker reads this at its next bootstrap and forces `origin/main` as the checkout start point instead of re-attaching to the retired content — the origin `card/<id>` ref itself is separately backed up then deleted by the worker as a lazy hygiene step. Idempotent: retiring an already-retired branch just re-stamps reason/actor/timestamp.", {
542
+ id: z.string().min(1),
543
+ reason: z.string().min(1),
544
+ ...boardField,
545
+ }, async (args) => jsonResult(await issueRetireBranch(client, args)));
542
546
  // ---------------- issue_quality_gate ----------------
543
547
  server.tool("issue_quality_gate", "Set one card's per-gate `required` flag via POST /api/issues/:id/quality-gates/:gate {required} — the only post-create way (issue_create takes gate_decisions; issue_edit refuses gate keys). PRE `plan-*` gates run before the work dispatch; POST `code-*` gates block complete. Unknown gate → 400. The board state per gate is tri-state: `required` always runs, `optional` runs WHEN this flag is true (optional is NOT off), `disabled` never runs. Optional `effort_level` overrides a `plan-*` gate's reviewer rung (null clears it). The write always succeeds and returns `{issue, applied: true, effective, reason}` — read `effective` (does the gate now run) and `reason` (why the board overrode your value), not just the 200. Board-scoped; see `board`.", {
544
548
  id: z.string().min(1),
@@ -758,47 +762,6 @@ server.tool("plan_reorder_architecture_section", "Reassign your connected plan's
758
762
  .min(1)
759
763
  .describe("Every live section id of the connected plan, in the desired order — exactly once each."),
760
764
  }, async (args) => jsonResult(await planReorderArchitectureSection(client, args)));
761
- // ---------------- failure_category_list / _create / _update (DX-2792) ----------------
762
- const matcherField = z
763
- .object({
764
- sourceKind: z
765
- .enum(["tool-error", "hook-refusal", "api-error", "usage-limit", "session-result"])
766
- .optional()
767
- .describe("Absent matches any source kind."),
768
- tool: z.string().min(1).optional().describe("Exact tool name (e.g. \"Bash\"); absent matches any tool, including null."),
769
- regexPattern: z
770
- .string()
771
- .min(1)
772
- .optional()
773
- .describe("Regex source tested against the normalized excerpt (paths/UUIDs/timestamps/ports already stripped)."),
774
- })
775
- .describe("At least one of sourceKind/tool/regexPattern must be set — an empty matcher is refused.");
776
- const expectedRateField = z
777
- .object({
778
- count: z.number().int().min(0).describe("N — how many failures are expected."),
779
- overDispatches: z.number().int().positive().describe("X — over how many dispatches."),
780
- })
781
- .nullable()
782
- .optional()
783
- .describe("Both fields together, or omit/null entirely — never a half-specified rate.");
784
- server.tool("failure_category_list", "List every failure category via GET /api/failure-categories (DX-2791/DX-2792). Board-less — a category applies across the whole install, not one board. Returns `{categories: [{id, name, description, matchers, ignore, ignoreReason, expectedRate, matchedCount, lastSeenMs, createdAtMs, createdBy, updatedAtMs, updatedBy}]}`, id ascending. A fresh install returns `{categories: []}`. Read this immediately before `failure_category_create`/`failure_category_update` so your overlap/expand decision is against the CURRENT set, not a stale snapshot from earlier in the dispatch.", {}, async () => jsonResult(await failureCategoryList(client)));
785
- server.tool("failure_category_create", 'Create a new failure category via POST /api/failure-categories (DX-2791/DX-2792). Board-less. `matchers` (at least one) is an OR-across-matchers set — a category matches an occurrence when ANY ONE matcher\'s fields all hold. `ignore: true` REQUIRES a non-empty `ignoreReason` (400 otherwise) — use this for an expected, non-actionable failure rather than leaving it uncategorized. A matcher set overlapping an EXISTING category is refused 400 naming the conflict — call `failure_category_list` first and EXPAND that category (`failure_category_update`) instead of creating a near-duplicate. On success, the dashboard re-matches every existing uncategorized occurrence against the new category before responding.', {
786
- name: z.string().min(1).describe("The category's name."),
787
- description: z.string().optional().describe("Optional free-text description. Defaults to empty."),
788
- matchers: z.array(matcherField).min(1).describe("At least one matcher; a category matches an occurrence when ANY ONE matches (OR across matchers)."),
789
- ignore: z.boolean().optional().describe("Mark this category as expected/non-actionable noise. Requires ignoreReason. Defaults to false."),
790
- ignoreReason: z.string().nullable().optional().describe("Required (non-empty) when ignore is true."),
791
- expectedRate: expectedRateField,
792
- }, async (args) => jsonResult(await failureCategoryCreate(client, args)));
793
- server.tool("failure_category_update", "Patch an existing failure category via PATCH /api/failure-categories/:id (DX-2791/DX-2792). Board-less. This is the tool for BOTH actions: expanding an existing category's matchers (send the FULL replacement `matchers` array — it REPLACES, not appends, so include every matcher you want to keep alongside the new one) and marking a category ignored (`ignore: true` + a non-empty `ignoreReason`). At least one field besides `id` is required (400 otherwise). Same overlap refusal as create, excluding this category's own prior matchers. On success, re-matches every uncategorized occurrence against the updated matcher set before responding.", {
794
- id: z.number().int().positive().describe("The category id to patch — from failure_category_list."),
795
- name: z.string().min(1).optional(),
796
- description: z.string().optional(),
797
- matchers: z.array(matcherField).min(1).optional().describe("REPLACES the full matcher set when sent — never a partial append."),
798
- ignore: z.boolean().optional(),
799
- ignoreReason: z.string().nullable().optional(),
800
- expectedRate: expectedRateField,
801
- }, async (args) => jsonResult(await failureCategoryUpdate(client, args)));
802
765
  // ---------------- main ----------------
803
766
  async function main() {
804
767
  boot();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.72",
3
+ "version": "0.1.73",
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",