@thehammer/danx-dashboard-mcp 0.1.219 → 0.1.221

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/index.js CHANGED
@@ -89,6 +89,7 @@ import { planConnect, danxbotApi, danxbotApiList, danxbotApiSpec, attachFile, DA
89
89
  import { bodyValueSchema, queryValueSchema } from "./types.js";
90
90
  import { termsAcceptanceCallHalt, termsHaltFor } from "./terms-halt.js";
91
91
  import { danxbotApiExamplesText } from "./danxbot-api-examples.js";
92
+ import { assertWithinTextLimit } from "./text-limit.js";
92
93
  import { registerLifecycleTools, applyLifecycleVisibility, readLifecycleUrlsFromEnv, } from "./lifecycle.js";
93
94
  function readEnvOrDie(name) {
94
95
  const v = process.env[name];
@@ -220,61 +221,39 @@ function readSessionConfig() {
220
221
  return { id };
221
222
  }
222
223
  /**
223
- * DX-3465 (PLN-11 R-25) — the ONE home for prose that used to be repeated
224
- * (or would otherwise need repeating) on every per-operation tool this
225
- * server exposes, now that `danxbot_api` is the generic surface for
226
- * everything that is not agent-specific (R-25). Read this before your first
227
- * `danxbot_api` call.
224
+ * DX-3465 (PLN-11 R-25) — the conventions shared by every `danxbot_api` call,
225
+ * stated once here instead of on each tool.
228
226
  *
229
- * Per-tool descriptions stay minimal — this block is where the shared rules
230
- * live, once.
227
+ * DX-4318 — this block is the ONE home of the stale-hash rule and of the
228
+ * writing standard (PLN-11 R-24): it is the only text every caller of these
229
+ * tools receives (operator sessions, their sub-agents, every dispatch
230
+ * profile). It was 3,916 characters and Claude Code delivered the first
231
+ * 2,048, so both rules were cut from every session. `assertWithinTextLimit`
232
+ * below refuses a block that would be cut again. Behaviour only, no rationale.
231
233
  */
232
234
  const SERVER_INSTRUCTIONS = `\
233
- danx-dashboard-mcp — reach the dashboard API through danxbot_api; find a route with danxbot_api_list; discover
234
- its shape through danxbot_api_spec.
235
+ Conventions for every \`danxbot_api\` call. Find a route with \`danxbot_api_list\` and its exact shape with
236
+ \`danxbot_api_spec\`.
235
237
 
236
- UNIFORM RESOURCE SHAPE. Most dashboard resources (cards, plans, records, comments, problems, ...) follow the
237
- same four verbs:
238
- - list GET <collection> — filter/sort/limit/offset; the default row is minimal scalars,
239
- opt into more with \`fields\`.
240
- - get GET <collection>/:id — one resource; \`fields\` opts into relations/heavy data the same way.
241
- - create POST <collection> — the resource's own required fields; a 400 names any missing/invalid one.
242
- - update PATCH/PUT <collection>/:id — send only the fields you are changing; most resources require the
243
- concurrency token below when the edit touches contested text fields.
244
- A card is the exception: PATCH /api/issues/<id>/edit.
245
- - delete DELETE <collection>/:id — usually a soft-delete; the resource's own docs say so.
246
- Don't know the route for what you need to do? Call \`danxbot_api_list()\` — it lists exactly the routes your
247
- credential may call, one line each. Then call \`danxbot_api_spec({endpoints: ["METHOD /path/template"]})\` for the
248
- EXACT current shape of the one(s) you picked, before calling them through \`danxbot_api\` — request/response
249
- schema, required permission, and every documented error envelope. Never guess a field name or a route path.
238
+ RESOURCES. Most collections take GET (list) and POST (create); a single resource (\`<collection>/<id>\`) takes
239
+ GET, PATCH or PUT (send only the fields you change) and DELETE. A card is edited at
240
+ \`PATCH /api/issues/<id>/edit\`. A list or get answers scalars only: name each relation or heavy field you need in
241
+ the \`fields\` query tree. \`GET /api/resources/<resource>/fields\` lists the names.
250
242
 
251
- ALWAYS PASS \`fields\`. The default response for a list/get route is minimal — scalars only, no relations, no
252
- heavy text. Every resource that supports it takes a \`fields\` query param (a JSON field tree) that opts into exactly what you need. Passing nothing and then being surprised a field is
253
- missing is a caller bug, not a server one — check the spec for the resource's own field vocabulary.
243
+ STALE HASH. A write to contested text (a card's title, description or checklists, a plan record, a problem) sends
244
+ back the \`content_hash\` or \`base_hash\` you read from that resource. A stale hash is refused 409 with the current
245
+ hash and content: merge your change into that and retry with the fresh hash.
254
246
 
255
- THE STALE-HASH RULE. A write that touches contested text (a card's title/description/checklists, a plan
256
- record, a problem, ...) is optimistic-concurrency guarded by a \`content_hash\` / \`base_hash\` you must have read
257
- FIRST (from the same resource's own \`get\`/\`list\` response) and send back unchanged. A stale hash 409s with the
258
- CURRENT hash and CURRENT content in the body — re-read, merge, retry with the fresh hash. Never retry blindly
259
- with the same stale value, and never omit the hash hoping the write "just works" — omission is refused exactly
260
- like a stale value on any route that requires one.
247
+ REFUSALS. A 4xx body is \`{error, ...}\`, and its other fields name the fix: \`offending_keys\`, \`failed_gate\`,
248
+ \`unresolved_items\`, \`did_you_mean\`. Read it before you retry.
261
249
 
262
- A 4XX BODY NAMES THE FIX. Every refusal is structured, never a bare string: \`{error, ...extra}\`, where the
263
- extra fields tell you exactly what to do next — \`offending_keys\` (an unknown/invalid field, by name),
264
- \`failed_gate\` (which lifecycle/dispatch gate blocked a transition), \`currentHash\`/\`currentContent\` (a stale-
265
- hash 409, ready to merge from), \`unresolved_items\` (which checklist items still block completion). Read the
266
- body before retrying — the shape is the same one \`danxbot_api_spec\` documents per route, so a persistent 4xx
267
- you don't understand means re-checking the spec, not repeating the same call.
268
-
269
- WRITING STANDARD — for any tool that writes prose onto a card, plan record, or problem (title, summary,
270
- description, context, a problem's statement, a solution's body, ...). The reader is new to the subject and has
271
- not read, and will not read, the code or docs: what you write IS their documentation. Define any term a
272
- newcomer would not know where it first appears. Open with plain background — what the thing is, why it exists,
273
- what is wrong or what the choice changes — before evidence or technical detail. Cite ids/SHAs/paths precisely
274
- rather than describing them vaguely ("the migration file" → the actual filename). Be brief: the goal is less to
275
- read, not more, not a transcript of your reasoning. A summary/title alone must let the reader decide without
276
- opening the description.
250
+ WRITING STANDARD, for every title, summary, description, comment, problem, solution and plan record you write. The
251
+ reader is new to the subject and will not read the code or docs: what you write is their documentation. Open with
252
+ plain background (what the thing is, why it exists, what is wrong or what the choice changes) before evidence.
253
+ Define each term a newcomer would not know. Cite exact ids, SHAs and paths. Be brief: less to read, never a
254
+ transcript of your reasoning. A title or summary alone must let the reader decide.
277
255
  `;
256
+ assertWithinTextLimit("the server instructions", SERVER_INSTRUCTIONS);
278
257
  export const server = new McpServer({
279
258
  name: "danx-dashboard-mcp",
280
259
  version: "0.1.0",
@@ -309,6 +288,8 @@ function jsonResult(value) {
309
288
  * set, exactly like the dashboard's own `parseFilterObject` refusal.
310
289
  */
311
290
  function strictTool(name, description, shape, cb) {
291
+ // DX-4318 — the one registration path, so no tool can ship a description an agent would receive cut.
292
+ assertWithinTextLimit(`the "${name}" tool description`, description);
312
293
  return server.registerTool(name, { description, inputSchema: z.object(shape).strict() }, cb);
313
294
  }
314
295
  // ---------------- lifecycle tool set (DX-3465 / PLN-11 R-25) ----------------
@@ -342,15 +323,11 @@ const boardField = {
342
323
  // `danxbot_api({method:"GET", path:"/api/resources/<resource>/fields"})`
343
324
  // instead; the route and its response shape are unchanged.
344
325
  // ---------------- danxbot_api (DX-3465, PLN-11 R-25) ----------------
345
- strictTool("danxbot_api", "Call any dashboard API route your credential may reach. `method` + `path` (a FULL path, e.g. " +
346
- '"/api/issues/DX-1/transition" — substitute real ids yourself, never a route template with `:id` in it) + ' +
347
- // DX-3997 — `reminders[]` also relays the dashboard's own listener reminder
348
- // to a plan-connected session; the reminder's text explains itself.
349
- "optional `query`/`body`. The response is the route's JSON body verbatim, `reminders[]` included when the " +
350
- "route attaches one. `board` overrides this dispatch's configured board for a board-scoped route only. " +
351
- "See this server's own instructions (shown once, at connect) " +
352
- "for the shared resource shape, the stale-hash rule, and how to read a refusal — don't know the route? call " +
353
- "`danxbot_api_list` first. " +
326
+ strictTool("danxbot_api",
327
+ // DX-3997 — `reminders[]` also relays the dashboard's own listener reminder
328
+ // to a plan-connected session; the reminder's text explains itself.
329
+ "Call any dashboard API route your credential may reach. The answer is the route's JSON body: act on its " +
330
+ "`reminders[]` when present. " +
354
331
  // DX-4015 — the ONE statement of which writes need a spec call first; the
355
332
  // `work` profile's spec-first sentence defers to this list. A shown write
356
333
  // is copied from its example (3 of 3 canary sessions did, with zero 4xx).
@@ -367,7 +344,7 @@ strictTool("danxbot_api", "Call any dashboard API route your credential may reac
367
344
  .describe('Full path starting with "/api/" — never a route template.'),
368
345
  // DX-3465 — a query value may be a scalar, a field TREE (a plain
369
346
  // object, e.g. `{"description":true,"comments":{"limit":20}}` — see
370
- // SERVER_INSTRUCTIONS below and the OpenAPI spec's `fields` schema), or
347
+ // SERVER_INSTRUCTIONS above and the OpenAPI spec's `fields` schema), or
371
348
  // an array of scalars (e.g. `ids` on the batch-get route).
372
349
  // `DashboardHttpClient#buildUrl` JSON-encodes a plain object but
373
350
  // comma-joins an array, matching the two shapes the server actually
@@ -401,22 +378,18 @@ async (args) => {
401
378
  return jsonResult(await listenerReminder.apply(result));
402
379
  });
403
380
  // ---------------- danxbot_api_list (DX-3465 A5, PLN-11 R-25) ----------------
404
- strictTool("danxbot_api_list", "Lists the dashboard API routes your credential may call right now — `[{method, path, summary, permission}]`, " +
405
- "one line each, in the route TEMPLATE form `danxbot_api_spec` also expects (e.g. " +
406
- '`"POST /api/issues/:id/transition"`). This is how you find the route for something you need to do: a ' +
407
- "route listed here is always callable by you; one that isn't never appears (DX-3528 filters through the " +
408
- "SAME permission check the dashboard enforces on every request, so this list can never drift from what " +
409
- "you can actually call). No arguments. Call this when you don't already know the route, then " +
410
- "`danxbot_api_spec({endpoints})` for the exact contract of the one(s) you picked, then `danxbot_api` to " +
411
- "call it.", {}, async () => jsonResult(await danxbotApiList(client)));
381
+ strictTool("danxbot_api_list",
382
+ // DX-3528 — the list is filtered by the same permission check every request takes.
383
+ "Lists the dashboard API routes your credential may call: `[{method, path, summary, permission}]`, each " +
384
+ "`path` a route template (`/api/issues/:id/transition`), the form `danxbot_api_spec` takes. A route not " +
385
+ "listed is not callable by you.", {}, async () => jsonResult(await danxbotApiList(client)));
412
386
  // ---------------- danxbot_api_spec (DX-3465, PLN-11 R-25) ----------------
413
- strictTool("danxbot_api_spec", "The exact OpenAPI 3.1 contract (DX-3527) for one or more routes — request/response shape, required " +
414
- "permission, and every documented error envelope. `endpoints` (REQUIRED, non-empty): an array of " +
415
- '`"METHOD /path/template"` strings (e.g. `"POST /api/issues/:id/transition"`, the route TEMPLATE form, ' +
416
- "unlike `danxbot_api`'s own `path` which is a real path with real ids — the same form `danxbot_api_list` " +
417
- "reports). A requested endpoint this dashboard does not serve, or that your credential may not call, is " +
418
- "refused loud (never silently dropped or silently included) — see the response for which. Don't know which " +
419
- "endpoints to name? call `danxbot_api_list` first.", {
387
+ strictTool("danxbot_api_spec",
388
+ // DX-3527 — the contract is generated from the route table.
389
+ "The OpenAPI 3.1 contract for the routes you name: request and response shape, required permission and " +
390
+ 'every documented error. `endpoints`: `"METHOD /path/template"` strings as `danxbot_api_list` reports them ' +
391
+ '(e.g. `"POST /api/issues/:id/transition"`), never a real path with ids. An endpoint the dashboard does ' +
392
+ "not serve, or your credential may not call, is refused and named.", {
420
393
  endpoints: z.array(z.string().min(1)).min(1),
421
394
  }, async (args) => jsonResult(await danxbotApiSpec(client, args)));
422
395
  // ---------------- attach_file (DX-3465 A4, checklist item 36140) ----------------
@@ -429,18 +402,12 @@ strictTool("danxbot_api_spec", "The exact OpenAPI 3.1 contract (DX-3527) for one
429
402
  // out plus a role-by-role grant, see `tool-defs.ts`'s `MCP_SERVER_OPT_IN_TOOLS`
430
403
  // — a role needs local-file-upload only when it deals with files an agent
431
404
  // might hold locally, e.g. the Slack recovery agent's CSV path).
432
- strictTool("attach_file", "Attach a LOCAL file to a card: `card_id` + `path` (an ABSOLUTE path on the dispatch's shared filesystem). " +
433
- "Reads the bytes INSIDE this MCP server process and uploads them — the file's bytes never pass through " +
434
- "your own context/transcript. Infers MIME from the extension unless you pass `content_type`; defaults " +
435
- "`filename` to the path's basename unless you pass one. Stores in S3, inserts one attachment row, auto-" +
436
- "mirrors to the card's linked Trello card and Slack thread. Board-scoped; see `board`. Fail-loud: a " +
437
- "relative/empty path is rejected before any upload; a missing/unreadable file throws before any upload " +
438
- "(no partial S3 object or row). 25 MB decoded ceiling (413). Returns `{issue, attachment_id, s3_key}` — " +
439
- "find the attachment in `issue.attachments` by `attachment_id` to read its permanent, non-expiring `url`. " +
440
- "That url can be embedded in ANY markdown field (a problem's statement/context, a comment, a plan record, " +
441
- "a solution body) as `![description](url)` — the dashboard renders it inline as a thumbnail. Use whenever " +
442
- "a screenshot or a local file judges something faster than prose, or replaces pasting a large CSV/log " +
443
- "straight into a card field.", {
405
+ strictTool("attach_file", "Attach a LOCAL file to a card. This server reads and uploads the bytes, so they never pass through your " +
406
+ "context. The attachment mirrors to the card's linked Trello card and Slack thread. 25 MB limit (413). " +
407
+ "Returns `{issue, attachment_id, s3_key}`: the entry of `issue.attachments` with that `attachment_id` " +
408
+ "carries a permanent `url`: embed it in any markdown field as `![description](url)` and it renders " +
409
+ "inline. Use it when a screenshot or file judges something faster than prose, or instead of pasting a " +
410
+ "large CSV or log into a card field.", {
444
411
  card_id: z.string().min(1).describe("The card id to attach the file to."),
445
412
  path: z
446
413
  .string()
@@ -461,14 +428,21 @@ strictTool("attach_file", "Attach a LOCAL file to a card: `card_id` + `path` (an
461
428
  // a different reason: it MAKES a plan rather than acting on one, so there is
462
429
  // no existing plan for an id to name yet.
463
430
  strictTool("plan_connect",
464
- // DX-2735: trimmed with the problem tools to stay inside the work-profile budget.
465
- "Connect THIS session to a plan. ONE CALL IS ENOUGH TO START: the reply carries `{session, movedFrom, browserInstruction, briefing, listenerHealth, missedEvents}` — `missedEvents` is `{events, omitted}`: the operator events on this plan that no session had been told about before you connected, oldest first, capped to the newest few (`omitted` counts the older ones left out) — read them before anything else. `browserInstruction` is a server-built action to take NOW: it names the plan's URL and tells you to open it (in-app browser if you have one, else default), keep that tab open for the whole session without navigating it away, and use a different tab for your own browsing — the operator's tab for following and talking to you. Returned on every connect, including a re-connect, so it doubles as the post-context-loss reminder. `briefing` is every goal/rule/caveat (ref+body), every architecture section (id+title), the plan's own ref/name/status, a first page of open cards (id/type/status/title/openProblemCount/assignedAgent) with a `morePagesHint` when more exist, and the closed-card count — usually replacing the plan read + card batch-read a fresh session used to need. A session is on AT MOST ONE plan: connecting elsewhere MOVES it, and `movedFrom: {id, name} | null` says which plan it left (null = no plan, or already this one). Binds only your OWN session, resolved from the session id this server forwards; afterwards a `danxbot_api` call to a `/api/plans/mine/...` route (there is no dedicated plan-write tool) acts on this plan and takes no plan id. `listenerHealth` reports the event bridge's state — `null` (no session), or `{attached, state: \"unattached\"|\"credential_mismatch\"|\"healthy\", nextStep}` naming a concrete fix per unhealthy state (same shape the plan list/get routes report). Once healthy, every comment, answer, problem added and block/unblock on this plan's cards reaches the session unpolled, relayed by the plugin's event bridge as a line like `[DX-8 \"Title\" repo:board] newms87 answered \"<statement>\": chose \"Pause E2E\"`. An action the operator DECLINES arrives as `... rejected the action \"<title>\" for \"<statement>\" — reason: \"<note>\". Do not perform it; re-plan from the reason` (a decision with `outcome: \"rejected\"` — never treat it as work done). Pass `title` (call `get_session({session_id:\"self\"})` first and forward its `title` verbatim) so the dashboard shows the same name Claude does — this server cannot read it itself. The response's `reminders[]` (when present) carries the mantra — read and follow it. To LEAVE: `{plan_id: <current plan>, disconnect: true}` returns `{session, leftPlan}`; a wrong plan is a 409.", {
431
+ // DX-4318 — states only what to do with the reply. What it used to add arrives where it is used: a rejected
432
+ // action's rule in the event line itself (`listen.ts`), the mantra in the reply's own `reminders[]` text.
433
+ "Connect THIS session to a plan; one call is enough to start. The reply carries `missedEvents` (operator " +
434
+ "events on the plan that no session had been told about, oldest first: read them first), " +
435
+ "`browserInstruction` (do it now), `briefing` (the plan's goals, rules, caveats, architecture section " +
436
+ "titles and a first page of open cards) and `listenerHealth` (when its `state` is not `healthy`, do its " +
437
+ "`nextStep`). A session is on at most one plan: connecting elsewhere moves it, and `movedFrom` names the " +
438
+ "plan it left. Afterwards a `danxbot_api` call to a `/api/plans/mine/...` route acts on this plan and " +
439
+ "takes no plan id.", {
466
440
  plan_id: z.number().int().positive().describe("The plan id, from the plans list (`danxbot_api` `GET /api/plans`)."),
467
441
  title: z
468
442
  .string()
469
443
  .min(1)
470
444
  .optional()
471
- .describe("THIS session's own Claude session title — from `get_session({session_id:\"self\"}).title`, passed verbatim, never invented or derived from the repo/cwd. Stored on your session's row: a title different from what is already stored UPDATES it; omit to leave the stored title untouched. Not this plan's name. At most 200 characters — an overlong title is refused with a 400 naming its length."),
445
+ .describe('This session\'s own Claude title: `get_session({session_id:"self"}).title`, verbatim, never invented and never the plan\'s name. Omit to keep the stored title.'),
472
446
  disconnect: z.boolean().optional().describe("true = leave `plan_id` (your current plan); no `title`."),
473
447
  },
474
448
  // DX-2862 — the connect is also the moment this session's event bridge learns
package/dist/lifecycle.js CHANGED
@@ -181,10 +181,10 @@ export function registerLifecycleTools(strictTool) {
181
181
  "cancelled = agent voluntarily abandons the dispatch (does NOT cancel the card — transition it first); " +
182
182
  "critical_failure = environment broken in a way that affects every dispatch (MCP tools not loading, " +
183
183
  "Bash unavailable, Claude auth missing) — halts the poller until a human clears it; " +
184
- "waiting_on_dependency = genuinely blocked on a card that must land on another board/repo — file that " +
185
- "change as a card on the right board, add a depends_on edge from yours to it, commit your own WIP, " +
186
- "transition yours rollback_pickup, then end with this status (never complete/ready/archive/review, " +
187
- "which delete your branch)."),
184
+ // DX-4318 — the steps live in each profile that grants this ending (`work` and `failure-repair`,
185
+ // `required-profiles.yaml`), never repeated here.
186
+ "waiting_on_dependency = blocked on a card that must land on another board or repo: do your " +
187
+ "profile's steps for work that needs another repo or board's change first."),
188
188
  summary: z
189
189
  .string()
190
190
  .describe("Brief summary of what happened. For failed, why the dispatch stopped, " +
@@ -213,16 +213,16 @@ export function registerLifecycleTools(strictTool) {
213
213
  };
214
214
  });
215
215
  // ---------------- danxbot_slack_reply ----------------
216
- const slackReply = strictTool("danxbot_slack_reply", "Post the FINAL user-facing reply to the originating Slack thread. Call exactly once per dispatch, " +
217
- "immediately before danxbot_complete. Optionally attach result files via files[] (absolute paths of files on YOUR own " +
218
- "disk — their bytes are uploaded, so this works whichever host the dashboard runs on) — text + attachments " +
219
- "land as ONE atomic thread post. For intermediate " +
220
- "progress use danxbot_slack_post_update instead.", {
216
+ const slackReply = strictTool("danxbot_slack_reply",
217
+ // DX-4253 — no ordering against another tool here: the Slack persona (`src/agent/persona.ts`) and the
218
+ // `slack-agent` profile own the order (reply, link, attach, complete).
219
+ "Post the final user-facing reply to the originating Slack thread, once per dispatch. Files in `files[]` " +
220
+ "post with the text as one thread message. For progress updates use danxbot_slack_post_update.", {
221
221
  text: z.string().describe("The final reply text to post to the Slack thread."),
222
222
  files: z
223
223
  .array(z.string())
224
224
  .optional()
225
- .describe("Optional absolute paths of files on your own disk to attach; their bytes are uploaded."),
225
+ .describe("Absolute paths of your own files to attach; bytes are uploaded."),
226
226
  }, async (args) => {
227
227
  const url = readLifecycleUrlsFromEnv().slackReply;
228
228
  if (!url)
@@ -350,27 +350,21 @@ export function registerLifecycleTools(strictTool) {
350
350
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
351
351
  });
352
352
  // ---------------- quality_gate_complete (absorbs quality_gate_skip, DX-3394 problem 1682) ----------------
353
- const qualityGateComplete = strictTool("quality_gate_complete", "Sign off a registered quality gate. NEVER visible to (or callable by) the WORK dispatch that authored " +
354
- "the diff — DX-3741: every gate, PLAN or CODE, runs as its OWN independent gate-reviewer dispatch, so " +
355
- "seeing this tool at all means you ARE that reviewer, not the author. Two flavors by the gate's PHASE: " +
356
- "(PLAN, e.g. plan-architecture) — reviews the CARD's plan before any code exists; this call is your " +
357
- "dispatch's TERMINAL call: status='pass' releases the dispatch (the picker launches the next gate or " +
358
- "the work agent); status='fail' BLOCKS the card (your message becomes the reason) and stops the " +
359
- "dispatch (do NOT follow up with danxbot_complete); status='retry' — the fixer loop: you edited the " +
360
- "card into a sounder plan and want a fresh pass. (CODE, e.g. code-quality) — reviews `git diff " +
361
- "origin/main...HEAD` on the author's `card/<id>` branch, already pushed to origin; this call is ALSO " +
362
- "your dispatch's TERMINAL call: report every finding via `findings[]`; status='fail' releases the " +
363
- "dispatch and — BELOW the per-gate review-pass cap — does NOT block the card: the author's WORK " +
364
- "session, re-dispatched, reads your findings, fixes them, and calls request_code_review again, which " +
365
- "re-dispatches every requested code gate fresh; AT the cap (3 review passes, the runaway backstop) a " +
366
- "'fail' DOES block the card instead, exactly like a PLAN-phase fail — you are not told which round " +
367
- "this is, so sign 'fail' on the findings alone and let the dispatcher enforce the cap; " +
368
- "status='pass' releases the dispatch toward the next requested code gate or back to WORK to finalize; " +
369
- "status='retry' is PLAN-phase only, refused here. status='skipped' — the gate is OBVIOUSLY " +
370
- "UNNECESSARY for this diff (a single-line / docs-only change, or nothing in this gate's lane): use " +
371
- `\`message\` as the mandatory justification (≥${QUALITY_GATE_SKIP_REASON_MIN_CHARS} chars, the ` +
372
- "permanent accountability record a human will read) — never skip to dodge a real review. You never " +
373
- "edit files and never transition the card yourself, on either phase.", {
353
+ const qualityGateComplete = strictTool("quality_gate_complete",
354
+ // DX-3741 — every gate runs as its own gate-reviewer dispatch, the only one that sees this tool. DX-4318 —
355
+ // what a status does, per phase; how to review (read-only, no transition) is the gate-reviewer profile's
356
+ // (`required-profiles.yaml`). The "3-pass cap" mirrors CODE_GATE_MAX_REVIEW_PASSES
357
+ // (`src/issues/quality-gates/registry.ts`), which this separately published package cannot import;
358
+ // `src/tokens/tool-defs.test.ts` compares the two.
359
+ "Sign off the quality gate you are reviewing. It is your dispatch's TERMINAL call: never follow it with " +
360
+ "danxbot_complete. PLAN gate: `pass` releases the card to the next gate or the work agent; `fail` blocks " +
361
+ "the card with `message` as the reason; `retry` means you edited the card into a sounder plan and want " +
362
+ "a fresh pass. CODE gate: report every finding in `findings[]`; `fail` returns the findings to the " +
363
+ "author, who fixes them and requests review again, and at the 3-pass cap blocks the card instead (sign " +
364
+ "on the findings alone; the dispatcher enforces the cap); `pass` releases toward the next code gate or " +
365
+ "the author's finalize; `retry` is refused. `skipped`: the gate obviously does not apply to this diff " +
366
+ "(one line, docs only, nothing in its lane), with `message` as the mandatory justification of at least " +
367
+ `${QUALITY_GATE_SKIP_REASON_MIN_CHARS} characters; never skip to dodge a real review.`, {
374
368
  gate: z.string().describe("Registered gate name, e.g. 'plan-architecture' or 'code-quality'."),
375
369
  status: z.enum(["pass", "fail", "retry", "skipped"]),
376
370
  message: z
@@ -0,0 +1,24 @@
1
+ /**
2
+ * DX-4318 — Claude Code delivers at most 2,048 characters (string length, not
3
+ * bytes) of an MCP server's instructions and of each tool description, then
4
+ * appends `… [truncated]`; nothing fails. Measured 2026-10-03 on three texts
5
+ * of this server, each cut at index 2,048. The limit here keeps 48 characters
6
+ * of headroom under that cut.
7
+ *
8
+ * Whether Claude Code also cuts a parameter description is unverified: none
9
+ * is long enough to show it. The package test holds them to the same limit, so
10
+ * the question stays moot.
11
+ */
12
+ export const MCP_TEXT_LIMIT_CHARS = 2000;
13
+ /**
14
+ * Refuses a text an agent would receive cut. Called where each text is
15
+ * registered, so an over-long one stops the module from loading: the package
16
+ * tests, the `tool-defs.json` generator and the publish boot check all fail
17
+ * on it.
18
+ */
19
+ export function assertWithinTextLimit(label, text) {
20
+ if (text.length > MCP_TEXT_LIMIT_CHARS) {
21
+ throw new Error(`[danx-dashboard-mcp] ${label} is ${text.length} characters, over the ${MCP_TEXT_LIMIT_CHARS}-character limit: ` +
22
+ `Claude Code would cut it and the agent would never see the rest. Shorten it.`);
23
+ }
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.219",
3
+ "version": "0.1.221",
4
4
  "description": "Stdio MCP server fronting danxbot's dashboard API for agents (operator sessions and dispatched workers alike): the generic danxbot_api/danxbot_api_list/danxbot_api_spec tools reach any route the caller's credential may call, plus dedicated agent-specific tools (plan_connect, danxbot_complete, and other per-dispatch lifecycle callbacks) (DX-3465, PLN-11 R-25).",
5
5
  "license": "MIT",
6
6
  "type": "module",