@thehammer/danx-dashboard-mcp 0.1.129 → 0.1.132

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.
Files changed (2) hide show
  1. package/dist/index.js +59 -61
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -343,14 +343,19 @@ const boardField = {
343
343
  // The three prose fields of a card, each with ONE job. Shared by issue_create
344
344
  // (root + phase children) and issue_edit so the guidance an agent reads is
345
345
  // identical wherever it writes the field.
346
- const TITLE_DESCRIBE = 'Short, specific label naming the domain (e.g. "Guest checkout rejects carts holding a gift card"). Never generic ("Fix bug", "Follow-up").';
347
- const SUMMARY_DESCRIBE = "1–3 plain-language sentences, no markdown/jargon: what the card is and why it matters. Always shown, never collapsed — not a second title, not a teaser for the description.";
348
- const DESCRIPTION_DESCRIBE = 'The full body ("Context"): evidence, examples, technical detail; markdown, collapsed by default. Operator questions go in issue_problem, not here.';
346
+ const TITLE_DESCRIBE = 'Short, specific label naming the domain (e.g. "Guest checkout rejects gift-card carts"). Never generic ("Fix bug", "Follow-up").';
347
+ const SUMMARY_DESCRIBE = "1–3 plain-language sentences, no markdown/jargon: what the card is and why it matters. Always shown, never collapsed — not a second title, not a teaser.";
348
+ const DESCRIPTION_DESCRIBE = 'The full body ("Context"): evidence, examples, technical detail; markdown, collapsed by default. Operator questions go in issue_problem. Style: see issue_comment\'s `text` param.';
349
+ // DX-3335 — condensed from the now-deleted danxbot:comment-style skill (PLN-11
350
+ // R-16: fold a skill's rule into the tool description it governs rather than
351
+ // require a separate load). Shared by every prose-writing field below so an
352
+ // agent sees the same rules wherever it writes card markdown.
353
+ const MARKDOWN_STYLE_DESCRIBE = "Renders as markdown here. Use `##`/`###` headers, fenced code blocks with a language tag, inline code for paths/symbols/flags, `-`/`1.` lists, and GFM tables (`|---|`) for 2D data — never ASCII tables. Never escape markdown characters (`\\*`, `\\_`) to \"play it safe\"; they render wrong. Don't repeat the same content as both prose and a list. Structure the content itself per `base:convey` (concept-first headline, diff, caveats, verify) — this rule only governs the markdown syntax.";
349
354
  // ---------------- issue_list ----------------
350
355
  strictTool("issue_list",
351
356
  // DX-2735: trimmed to pay for the problem tools inside the work-profile
352
357
  // injected-surface budget — same facts, no repeated prose.
353
- "List cards via GET /api/issues. Board-scoped; see `board`. `filter`: type, parent_id, dispatchable_derived, status_derived[], self_dispatchable_derived, assigned_agent, include_closed, include_deleted, q (free text over id+title+description). THE DEFAULT ROW IS MINIMAL — scalars only (id, type, title, status, parent_id, priority, created_at, updated_at, assigned_agent, content_hash), no joins; opt into heavy data with `fields` groups: description (+ summary), problems (open_problem_count — a card needs a human exactly when this is > 0), ac, comments, retro, dependencies, triage, assignment, quality_gates, children, effort. `sort`: [{column, order}] over id|priority|repo_name|title|type|status_derived|triage_ice_total|created_at|updated_at; default priority desc (highest priority=most urgent first), repo_name asc, numeric-id tiebreaker. `limit`/`offset` page (uncapped by default). DX-3113 — `include_closed` defaults to FALSE: a bare call silently excludes every Done/Cancelled card (leaf AND container alike). The response always carries `total` (the full count matching every filter except limit/offset — compare against `issues.length` to tell an exhausted list from a truncated one) and, whenever `include_closed` was not explicitly `true`, `closed_excluded` (how many additional terminal cards the default withheld — re-call with `include_closed: true` to see them). issue_get reads one card in full.", {
358
+ "List cards via GET /api/issues. Board-scoped; see `board`. `filter`: type, parent_id, dispatchable_derived, status_derived[], self_dispatchable_derived, assigned_agent, include_closed, include_deleted, q (free text over id+title+description). THE DEFAULT ROW IS MINIMAL — scalars only (id, type, title, status, parent_id, priority, created_at, updated_at, assigned_agent, content_hash), no joins; opt into heavy data with `fields` groups: description (+ summary), problems (open_problem_count — a card needs a human exactly when this is > 0), ac, comments, retro, dependencies, triage, assignment, quality_gates, children, effort. `sort`: [{column, order}] over id|priority|repo_name|title|type|status_derived|triage_ice_total|created_at|updated_at; default priority desc (highest=most urgent first), repo_name asc, numeric-id tiebreaker. `limit`/`offset` page (uncapped by default). DX-3113 — `include_closed` defaults to FALSE: a bare call silently excludes every Done/Cancelled card (leaf AND container alike). The response always carries `total` (the full count matching every filter except limit/offset — compare against `issues.length` to tell an exhausted list from a truncated one) and, whenever `include_closed` was not explicitly `true`, `closed_excluded` (how many additional terminal cards the default withheld — re-call with `include_closed: true` to see them). issue_get reads one card in full.", {
354
359
  filter: z
355
360
  .object({
356
361
  q: z.string().optional(),
@@ -384,7 +389,7 @@ strictTool("issue_list",
384
389
  strictTool("issue_get",
385
390
  // DX-2735: trimmed to pay for the problem tools inside the work-profile
386
391
  // injected-surface budget — same facts, no repeated prose.
387
- "Fetch one card (GET /api/issues/:id, `id`) or many (GET /api/issues/batch, `ids`, at most " + ISSUE_BATCH_GET_MAX + " — split larger sets); pass exactly one. Ids are globally unique, so both resolve from any board; the batch form is global and throws with `board`. THE DEFAULT ROW IS MINIMAL — scalars only (id, type, title, status, parent_id, priority, created_at, updated_at, assigned_agent, content_hash: the concurrency token issue_edit needs for title/description/checklists). `fields` opts into groups, per card in a batch too: description (body + summary), problems (live problems in order, each {id, statement, content_hash, open} with its solutions[] and decisions[] — a card needs a human exactly when open_problem_count > 0), ac (acceptance criteria + checklists), comments (see `comments_limit`/`comments_offset` below), retro, dependencies (waiting_on/conflict_on/blocked), triage (history + ICE), assignment (dispatch, assigned_agent, lifecycle timestamps), quality_gates (one row per gate {gate, required, status pending|pass|fail, completed_at, message}; a required PRE gate not `pass` pre-empts the work dispatch, and complete refuses while a required POST gate is not `pass`), children (ids + rollups), mirrors (external sync state), code_review_items, plans (every plan this card is on, `{id, ref, name}[]` via `plan_cards` — `ref` is the plan's `PLN-<id>`). The `comments` group returns a PAGE ANCHORED AT THE NEWEST COMMENT: `comments_offset` counts back from the newest comment (offset 0 = the most recent comments_limit comments), and `comments_total` is the card's real total comment count, so `comments_offset + comments.length < comments_total` means older comments remain — page forward with `comments_offset` to reach them; a long-running card's decisive history (what was tried, measured, rejected or reverted) often lives past the first page. WITHIN a page, comments are ordered CHRONOLOGICALLY (oldest first, newest last) — the same order every other comment read in this API uses; only the WINDOW you request is anchored at the newest end, not the array itself. `comments_limit`/`comments_offset` apply to the SINGLE-id form only (no per-card paging in the batch form — naming either alongside `ids` is refused). Single form: unknown id → 404. Batch form: `{issues: [...], not_found: [...ids]}` — an unknown or deleted id never fails the call.", {
392
+ "Fetch one card (GET /api/issues/:id, `id`) or many (GET /api/issues/batch, `ids`, at most " + ISSUE_BATCH_GET_MAX + " — split larger sets); pass exactly one. Ids are globally unique, so both resolve from any board; the batch form is global and throws with `board`. THE DEFAULT ROW IS MINIMAL — scalars only (id, type, title, status, parent_id, priority, created_at, updated_at, assigned_agent, content_hash: the concurrency token issue_edit needs for title/description/checklists). `fields` opts into groups, per card in a batch too: description (body + summary), problems (live problems in order, each {id, statement, content_hash, open} with its solutions[] and decisions[] — a card needs a human exactly when open_problem_count > 0), ac (acceptance criteria + checklists), comments (see `comments_limit`/`comments_offset` below), retro, dependencies (waiting_on/conflict_on/blocked), triage (history + ICE), assignment (dispatch, assigned_agent, lifecycle timestamps), quality_gates (one row per gate {gate, required, status pending|pass|fail, completed_at, message}; a required PRE gate not `pass` pre-empts the work dispatch, and complete refuses while a required POST gate is not `pass`), children (ids + rollups), mirrors (external sync state), code_review_items, plans (every plan this card is on, `{id, ref, name}[]` via `plan_cards` — `ref` is the plan's `PLN-<id>`). The `comments` group PAGES ANCHORED AT THE NEWEST COMMENT: `comments_offset` counts back from it (offset 0 = the most recent comments_limit comments); `comments_total` is the real total, so `comments_offset + comments.length < comments_total` means older comments remain — page forward to reach them; a long-running card's decisive history often lives past the first page. WITHIN a page, comments stay CHRONOLOGICAL (oldest first) — same order as every other comment read; only the requested WINDOW is anchored at the newest end. `comments_limit`/`comments_offset` apply to the SINGLE-id form only (naming either alongside `ids` is refused). Single form: unknown id → 404. Batch form: `{issues: [...], not_found: [...ids]}` — an unknown or deleted id never fails the call.", {
388
393
  id: z.string().min(1).optional(),
389
394
  ids: z.array(z.string().min(1)).min(1).max(ISSUE_BATCH_GET_MAX).optional(),
390
395
  fields: z
@@ -410,8 +415,8 @@ strictTool("issue_get",
410
415
  ...boardField,
411
416
  }, async (args) => jsonResult(await issueGet(client, args)));
412
417
  // ---------------- issue_create ----------------
413
- strictTool("issue_create", '`plan` is REQUIRED on every create (DX-3006, no default/inference): "mine" puts the card on THIS session\'s connected plan; null = deliberately no plan. "mine" while connected to none is refused (409 session_not_connected), creating NO card; a plan id is not accepted — a card is only ever created onto your own connected plan. Replaces the plan_add_card follow-up at creation time; plan_add_card still exists for putting an EXISTING card on a plan. ' +
414
- 'Create a card via POST /api/issues. Board-scoped; see `board`. type=Epic REQUIRES non-empty phase_children[] (epic and phases inserted in one transaction; children get the epic as parent); other types refuse phase_children[] (400). Status starts at Review. `list_id` places the card straight into a column — a board_lists id or the list\'s display NAME (case-insensitive, emoji-tolerant): a `ready`-type queue lands it in ToDo, a `completed` list in Done, with no follow-up transition. Not valid on Epic; unknown name/id → 400. `quality_gates` names the gates this card carries BEYOND the board\'s default set for its type — one `{gate, note?}` each. Omit for just the board defaults — an unnamed gate is simply not on the card. Add one later with `issue_quality_gate`. ALWAYS pass `triage_enabled` explicitly on the root card and every phase child: true only when it should enter automatic triage/dispatch without human review; absent → false.', {
418
+ strictTool("issue_create", '`plan` REQUIRED on every create (DX-3006, no default/inference): "mine" puts the card on THIS session\'s connected plan; null = deliberately no plan. "mine" while connected to none is refused (409 session_not_connected), creating NO card; a plan id is not accepted — a card is only ever created onto your own connected plan. Replaces the plan_add_card follow-up at creation time; plan_add_card still exists for putting an EXISTING card on a plan. ' +
419
+ 'Create a card via POST /api/issues. Board-scoped; see `board`. type=Epic REQUIRES non-empty phase_children[] (epic + phases inserted in one transaction, children get the epic as parent); other types refuse phase_children[] (400). Status starts at Review. `list_id` places the card straight into a column — a board_lists id or the list\'s display NAME (case-insensitive, emoji-tolerant): a `ready`-type queue lands it in ToDo, a `completed` list in Done, no follow-up transition needed. Not valid on Epic; unknown name/id → 400. `quality_gates` names the gates this card carries BEYOND the board\'s default set for its type — `{gate, note?}` each; omit for just the board defaults (an unnamed gate is simply not on the card; add one later with `issue_quality_gate`). ALWAYS pass `triage_enabled` explicitly on the root card and every phase child: true only when it should enter automatic triage/dispatch without human review; absent → false.', {
415
420
  type: z.enum(ISSUE_TYPES),
416
421
  title: z.string().min(1).describe(TITLE_DESCRIBE),
417
422
  summary: z.string().min(1).optional().describe(SUMMARY_DESCRIBE),
@@ -490,7 +495,7 @@ strictTool("issue_create", '`plan` is REQUIRED on every create (DX-3006, no defa
490
495
  ...boardField,
491
496
  }, async (args) => jsonResult(await issueCreate(client, args, config.board)));
492
497
  // ---------------- issue_edit ----------------
493
- strictTool("issue_edit", 'Patch a card via PATCH /api/issues/:id/edit. ALLOWED keys: title, summary, description, ac, checklists, effort_level, parent_id, priority, list_id, triage_enabled, type, content_hash. Any other key (lifecycle, triage, dependencies, retro, blocked) is refused 400 with offending_keys[] naming the right tool: issue_transition / issue_triage / issue_comment / issue_dependency / issue_problem / issue_retro. `type`: Story/Bug/Chore = eligible for autonomous pickup; Task or a container (Epic/Feature) removes that eligibility. `priority` (tier word "lowest"–"critical", or number 0–6, higher = more urgent) is the ONLY way to set priority; a "Priority:" line does nothing. CHECKLISTS: each item has one status `incomplete|failing|passing|cancelled|deferred`; `deferred` (work done, real-world check outstanding) REQUIRES `detail`, and a `📡`-prefixed item can never be `passing`. `ac` edits the default "Acceptance Criteria" checklist (items matched by check_item_id, else exact title); `checklists` REPLACES every named checklist with full status control (`{name, items:[{label, detail?, status}]}`). Send `ac` OR `checklists`, not both (400). `list_id` pins the card to a list by id or display NAME; its type must match the card\'s current derived status (400 otherwise — e.g. ready the card first before pinning it to a `ready`-type list); null clears the pin. `content_hash` (DX-2741) is the card\'s optimistic-concurrency token — REQUIRED whenever the edit touches `title` / `description` / `checklists` (NOT `ac`, which keeps its own check_item_id/title diffing): missing → 400, stale → 409 `stale_issue_content` carrying `currentHash` + `currentTitle` + `currentDescription`. Read it off `issue_get`/`issue_list`\'s `content_hash` scalar (present even minimal); on 409, re-`issue_get` and retry with the fresh hash — never blindly.', {
498
+ strictTool("issue_edit", 'Patch a card via PATCH /api/issues/:id/edit. ALLOWED keys: title, summary, description, ac, checklists, effort_level, parent_id, priority, list_id, triage_enabled, type, content_hash. Any other key (lifecycle, triage, dependencies, retro, blocked) is refused 400 with offending_keys[] naming the right tool: issue_transition / issue_triage / issue_comment / issue_dependency / issue_problem / issue_retro. `type`: Story/Bug/Chore = eligible for autonomous pickup; Task or a container (Epic/Feature) removes that eligibility. `priority` (tier word "lowest"–"critical", or number 0–6, higher = more urgent) is the ONLY way to set priority; a "Priority:" line does nothing. CHECKLISTS: each item has one status `incomplete|failing|passing|cancelled|deferred`; `deferred` (work done, real-world check outstanding) REQUIRES `detail`; a `📡`-prefixed item can never be `passing`. `ac` edits the default "Acceptance Criteria" checklist (items matched by check_item_id, else exact title); `checklists` REPLACES every named checklist with full status control (`{name, items:[{label, detail?, status}]}`). Send `ac` OR `checklists`, not both (400). `list_id` pins the card to a list by id or display NAME; its type must match the card\'s current derived status (400 otherwise — e.g. ready the card first before pinning it to a `ready`-type list); null clears the pin. `content_hash` (DX-2741) — the card\'s optimistic-concurrency token — is REQUIRED whenever the edit touches `title` / `description` / `checklists` (NOT `ac`, which keeps its own check_item_id/title diffing): missing → 400, stale → 409 `stale_issue_content` with `currentHash` + `currentTitle` + `currentDescription`. Read it off `issue_get`/`issue_list`\'s `content_hash` scalar (present even minimal); on 409, re-`issue_get` and retry with the fresh hash — never blindly.', {
494
499
  id: z.string().min(1),
495
500
  title: z.string().min(1).optional().describe(TITLE_DESCRIBE),
496
501
  summary: z
@@ -584,7 +589,7 @@ strictTool("issue_comment", "Comment CRUD via /api/issues/:id/comments[/:cid]. a
584
589
  id: z.string().min(1),
585
590
  action: z.enum(["add", "edit", "delete"]),
586
591
  comment_id: z.number().int().positive().optional(),
587
- text: z.string().min(1).optional(),
592
+ text: z.string().min(1).optional().describe(MARKDOWN_STYLE_DESCRIBE),
588
593
  metadata: z.record(z.unknown()).optional(),
589
594
  problem_id: z.number().int().positive().optional().describe("action=add only — thread this comment as a follow-up under a LIVE problem of this same card, without answering it"),
590
595
  ...boardField,
@@ -633,22 +638,20 @@ const STEP_INPUT = z.lazy(() => z
633
638
  .int()
634
639
  .positive()
635
640
  .optional()
636
- .describe("present -> this node IS an existing step (matched on this id, 400 if it does not resolve to a live " +
637
- "child of this exact parent scope — a supplied id is NEVER claimed by the fallback below); absent -> " +
638
- "id-less nodes match the first unclaimed sibling with the same title under the same parent, else " +
639
- "created new. This title-based match (never positional) is what lets an id-less MIDDLE insert keep " +
640
- "every later id-less sibling's id — send ids anyway whenever an existing step's identity matters " +
641
- "(checked progress, a later single-step edit), or when two siblings under the same parent share a title."),
641
+ .describe("present -> matches an existing step by this id (400 if not a live child of this exact parent scope, " +
642
+ "never claimed by the fallback below); absent -> matches the first unclaimed same-titled sibling " +
643
+ "under the same parent, else creates new (title-based, never positional — keeps later id-less " +
644
+ "siblings' ids intact on a middle insert). Send ids anyway when identity matters (checked progress, " +
645
+ "a later edit) or siblings share a title."),
642
646
  title: z
643
647
  .string()
644
648
  .min(1)
645
- .describe("one imperative line, the thing to do — labels (\"1.\", \"2a\") are DERIVED on read and must never appear " +
646
- "in the title itself."),
649
+ .describe("one imperative line — labels (\"1.\", \"2a\") are DERIVED on read, never put one in the title."),
647
650
  description: z.string().nullable().optional().describe("markdown detail, or null/omitted for none"),
648
651
  steps: z.array(STEP_INPUT).optional().describe("this step's own children, nested — refused past 3 levels total"),
649
652
  })
650
653
  .strict());
651
- strictTool("issue_problem", "A card's PROBLEMS via /api/issues/:id/problems[/:pid]: one statement the operator must resolve (a question, or a flaw in the plan) OR an action only a person can carry out, each with its own solutions/answers — one problem per question. OPEN = not yet answered; the card needs a human exactly while open_problem_count > 0 — there is no separate flag to set, adding a problem IS putting the card in front of a human. list → live problems in order, each {id, statement, context, type, summary, content_hash, open, solutions[], decisions[]}; add {statement, context?, type?, summary?, solutions?} → problem_id + solution_ids in one transaction (zero solutions is valid: the operator answers free-form) plus `problems_reminder: {open_problem_count, instruction}` naming each open problem's solution count; edit :pid {base_hash, statement, context?, summary?}; remove :pid {base_hash} — always allowed, even as the card's last open problem (removing it just means the card no longer needs a human). A stale base_hash → 409 `stale_problem` with currentHash + currentProblem: merge, then retry. No answer action — the operator answers in the dashboard. `statement` is capped at 200 characters (a 400 names the actual length otherwise): write it as ONE plain sentence, and put any investigation detail in `context` (markdown, no cap) instead of running it on — e.g. statement \"Which cache should we use?\", context \"Redis fits the read-heavy path; see benchmark in #123.\" not a run-on statement listing the whole investigation. Every problem is also a `type` with a `summary` distinct from `context` — read both fields' own descriptions below before your first `add`.", {
654
+ strictTool("issue_problem", "A card's PROBLEMS via /api/issues/:id/problems[/:pid]: one statement the operator must resolve (a question, or a flaw in the plan) OR an action only a person can carry out, each with its own solutions/answers — one problem per question. OPEN = not yet answered; the card needs a human exactly while open_problem_count > 0 — no separate flag, adding a problem IS putting the card in front of a human. list → live problems in order, each {id, statement, context, type, summary, content_hash, open, solutions[], decisions[]}; add {statement, context?, type?, summary?, solutions?} → problem_id + solution_ids in one transaction (zero solutions is valid: the operator answers free-form) plus `problems_reminder: {open_problem_count, instruction}` naming each open problem's solution count; edit :pid {base_hash, statement, context?, summary?}; remove :pid {base_hash} — always allowed, even as the card's last open problem (removing it just means the card no longer needs a human). Stale base_hash → 409 `stale_problem` with currentHash + currentProblem: merge, then retry. No answer action — the operator answers in the dashboard. `statement` capped at 200 characters (400 names the actual length otherwise): ONE plain sentence, with investigation detail in `context` (markdown, no cap) instead — e.g. statement \"Which cache should we use?\", context \"Redis fits the read-heavy path; see benchmark in #123.\" not a run-on statement. Every problem also has a `type` with a `summary` distinct from `context` — read both fields' descriptions before your first `add`.", {
652
655
  id: z.string().min(1),
653
656
  action: z.enum(["list", "add", "edit", "remove"]),
654
657
  problem_id: z.number().int().positive().optional().describe("edit/remove"),
@@ -669,16 +672,15 @@ strictTool("issue_problem", "A card's PROBLEMS via /api/issues/:id/problems[/:pi
669
672
  type: z
670
673
  .enum(["question", "action"])
671
674
  .optional()
672
- .describe("add only. Test: could I do this myself if I tried harder, missing only a decision? If yes, \"question\": " +
673
- "statement is the question (one plain sentence); summary optional (why it matters); context is the evidence; " +
674
- "solutions[] are candidate ANSWERS with pro/con, done the moment the operator picks one. If the blocker is " +
675
- "access, credentials, hardware, a human's authority, or a system you genuinely cannot reach, \"action\": " +
676
- "statement is WHAT IS TO BE DONE — the deed itself, never phrased as a question (\"Rotate the staging DB " +
677
- "credential\", not \"Should we rotate it?\"); summary is REQUIRED, saying WHY THE ACTION IS NEEDED AND WHY " +
678
- "YOU CANNOT DO IT YOURSELF (an add with no summary is refused 400 — proves this isn't you being lazy); " +
679
- "context is whatever detail the person needs to carry it out; solutions[] are the possible ROUTES a person " +
680
- "could take, and EACH MUST CARRY AT LEAST ONE STEP (refused 400 otherwise). Omitted defaults to \"question\" " +
681
- "— decide deliberately every time."),
675
+ .describe("add only. Test: could I do this myself if I tried harder, missing only a decision? Yes → \"question\": " +
676
+ "statement is the question (one plain sentence); summary optional; context is the evidence; solutions[] " +
677
+ "are candidate ANSWERS with pro/con, done once the operator picks one. Blocker is access, credentials, " +
678
+ "hardware, a human's authority, or a system you genuinely cannot reach → \"action\": statement is WHAT " +
679
+ "IS TO BE DONE — the deed, never a question (\"Rotate the staging DB credential\", not \"Should we " +
680
+ "rotate it?\"); summary REQUIRED — WHY NEEDED AND WHY YOU CANNOT DO IT YOURSELF (no summary → 400, " +
681
+ "proves this isn't laziness); context is detail for the person carrying it out; solutions[] are the " +
682
+ "possible ROUTES, EACH MUST CARRY AT LEAST ONE STEP (400 otherwise). Omitted defaults to \"question\" " +
683
+ "— decide deliberately."),
682
684
  summary: z
683
685
  .string()
684
686
  .nullable()
@@ -696,36 +698,32 @@ strictTool("issue_problem", "A card's PROBLEMS via /api/issues/:id/problems[/:pi
696
698
  ...boardField,
697
699
  }, async (args) => jsonResult(await issueProblem(client, args)));
698
700
  // ---------------- issue_solution ----------------
699
- strictTool("issue_solution", "One problem's options via /api/issues/:id/problems/:pid/solutions[/:sid], AND (DX-3310) one solution's individual " +
700
- "procedure steps via .../solutions/:sid/steps[/:stepId]; `problem_id` is REQUIRED (from issue_problem list/add; " +
701
+ strictTool("issue_solution", "One problem's options via /api/issues/:id/problems/:pid/solutions[/:sid], plus (DX-3310) a solution's own " +
702
+ "procedure steps via .../solutions/:sid/steps[/:stepId]; `problem_id` REQUIRED (from issue_problem list/add; " +
701
703
  "another problem's solution id → 404). add {title, body?, pro?, con?, recommended?, steps?}: title names the " +
702
- "option/route, body is its markdown detail, pro/con the case for and against, `steps` is its WHOLE procedure " +
703
- "authored in one call — an ordered array of {title, description?, steps?}, nestable to 3 levels (a 4th is refused " +
704
- "400 naming the offending step and the limit). Under an ACTION problem, `steps` must be non-empty " +
705
- "(refused 400 otherwise); under a QUESTION it may be omitted or empty. Labels (\"1\", \"2a\", \"2a.i\") are " +
706
- "ALWAYS DERIVED on read from position — never put one in a title yourself. edit :sid {base_hash, ...only the " +
707
- "changed fields, steps?, steps_base_hash?}: `steps` omitted leaves the stored procedure untouched, an explicit " +
708
- "array (including [], refused if it would leave an action's solution with zero steps) DIFFS against it — send " +
709
- "back an existing step's `id` (read from the card) to keep it (even while retitling/reordering it) and omit " +
710
- "`id` for a genuinely new step; any existing step you don't include gets removed. An id-less node matches the " +
711
- "first unclaimed sibling with the same title under the same parent (never positional), so an id-less MIDDLE " +
712
- "insert keeps every later id-less sibling's id — send ids anyway whenever an existing step's identity matters. " +
713
- "`steps_base_hash` is REQUIRED whenever `steps` is sent — the solution's current `steps_hash` (read it off the " +
714
- "card first); a stale value → 409 `stale_steps` carrying the current tree, so a step added/removed since your " +
715
- "read is never silently dropped. NEVER resend the whole tree just to fix one word — see add_step/edit_step/" +
716
- "remove_step below. remove :sid {base_hash}. A stale base_hash → 409 `stale_solution` with currentHash + " +
717
- "currentSolution: merge, then retry. At most ONE live recommended per problem: a second → 409 naming " +
718
- "`recommended_solution_id`. A CHOSEN option's words are frozen (409 — add a new one instead), and so is its " +
719
- "WHOLE PROCEDURE — every steps route below also refuses once a decision has chosen this solution.\n\n" +
720
- "Granular single-step actions — change ONE step without resending the tree: add_step {solution_id, title, " +
721
- "description?, parent_step_id?, position?} — title is ONE imperative line (labels are derived, never typed, " +
722
- "same rule as above); parent_step_id omitted/null = top-level; position is 1-indexed among the parent's current " +
723
- "live children, omitted = append, and a position beyond the current sibling count is refused (400), never " +
724
- "silently clamped to append; nesting past depth 3 → 400. edit_step {solution_id, step_id, base_hash, title?, " +
725
- "description?} — never moves a step (no parent_step_id/position here). remove_step {solution_id, step_id, " +
726
- "base_hash} — soft-deletes it AND its own live children; refused (409) if this solution belongs to an ACTION " +
727
- "problem and removing it would leave zero live steps. A stale base_hash on a step → 409 `stale_step` with " +
728
- "currentHash + currentStep (carrying its real derived `label`).", {
704
+ "option, body/pro/con its markdown detail and case for/against; `steps` authors the WHOLE procedure in one " +
705
+ "call — {title, description?, steps?}[], nestable to 3 levels (4th refused 400, naming the step + limit). " +
706
+ "ACTION problem: `steps` must be non-empty (400 otherwise); QUESTION: may be omitted/empty. Labels " +
707
+ "(\"1\", \"2a\", \"2a.i\") are DERIVED on read from position — never put one in a title. edit :sid {base_hash, " +
708
+ "...changed fields, steps?, steps_base_hash?}: `steps` omitted leaves the procedure untouched; an explicit " +
709
+ "array (incl. [], refused if it leaves an action's solution with zero steps) DIFFS against it — send an " +
710
+ "existing step's `id` to keep it (even while retitling/reordering), omit `id` for a new step; any existing " +
711
+ "step left out is removed. An id-less node matches the first unclaimed same-titled sibling under the same " +
712
+ "parent (never positional), so an id-less MIDDLE insert keeps later id-less siblings' ids — send ids anyway " +
713
+ "when identity matters. `steps_base_hash` REQUIRED whenever `steps` is sent — the solution's current " +
714
+ "`steps_hash`; stale → 409 `stale_steps` with the current tree. Never resend the whole tree to fix one word " +
715
+ "— use add_step/edit_step/remove_step below. remove :sid {base_hash}: stale → 409 `stale_solution` with " +
716
+ "currentHash + currentSolution, merge and retry. At most ONE live recommended per problem — a second → 409 " +
717
+ "naming `recommended_solution_id`. A CHOSEN option's words and WHOLE PROCEDURE are frozen (409 — add a new " +
718
+ "one instead); every steps route below refuses once a decision has chosen this solution.\n\n" +
719
+ "Granular single-step actions, no tree resend: add_step {solution_id, title, description?, parent_step_id?, " +
720
+ "position?} — title is one imperative line (labels derived, never typed); parent_step_id omitted/null = " +
721
+ "top-level; position 1-indexed among current live children, omitted = append, beyond sibling count → 400 " +
722
+ "(never clamped); nesting past depth 3 → 400. edit_step {solution_id, step_id, base_hash, title?, " +
723
+ "description?} — never moves a step. remove_step {solution_id, step_id, base_hash} — soft-deletes it and " +
724
+ "its live children; refused 409 if this solution belongs to an ACTION problem and removing it leaves zero " +
725
+ "live steps. Stale base_hash on a step → 409 `stale_step` with currentHash + currentStep (real derived " +
726
+ "`label`).", {
729
727
  id: z.string().min(1),
730
728
  action: z.enum(["add", "edit", "remove", "add_step", "edit_step", "remove_step"]),
731
729
  problem_id: z.number().int().positive(),
@@ -809,10 +807,10 @@ strictTool("issue_quality_gate_verdict", "Stamp an operator MANUAL quality-gate
809
807
  ...boardField,
810
808
  }, async (args) => jsonResult(await issueQualityGateVerdict(client, args)));
811
809
  // ---------------- issue_retro ----------------
812
- strictTool("issue_retro", "Replace the retro block via PUT /api/issues/:id/retro. Body: {good, bad, correctable_danxbot_problem, correctable_danxbot_problem_description, 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', few + expensive so listed explicitly). Each row: {name, kind:'group'|'e2e', num_tests, num_passing_tests, duration_ms} required; num_assertions + num_passing_assertions NULLABLE (vitest has no assertion totals — pass null or omit). `correctable_danxbot_problem` (DX-2794) is REQUIRED on every write, like tests[] — ANSWER IT HONESTLY: did THIS dispatch hit a problem in danxbot's own code or configuration (not merely \"this card was hard\") that danxbot could change so it stops happening? true REQUIRES a non-empty `correctable_danxbot_problem_description` naming the problem; false REQUIRES the description be empty. A true+described retro is read by a deterministic, no-LLM check and starts exactly one automated repair (fixes the problem or files a ready card) — this is the ONLY reliable channel for a danxbot defect found mid-dispatch to actually get fixed, so do not default to false out of haste.", {
810
+ strictTool("issue_retro", "Replace the retro block via PUT /api/issues/:id/retro. Body: {good, bad, correctable_danxbot_problem, correctable_danxbot_problem_description, 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, never individual unit tests) or per individual e2e test (kind:'e2e', few + expensive so listed explicitly). Each row: {name, kind:'group'|'e2e', num_tests, num_passing_tests, duration_ms} required; num_assertions + num_passing_assertions NULLABLE (vitest has no assertion totals — pass null or omit). `correctable_danxbot_problem` (DX-2794) is REQUIRED on every write, like tests[] — ANSWER HONESTLY: did THIS dispatch hit a problem in danxbot's own code or configuration (not merely \"this card was hard\") that danxbot could change so it stops happening? true REQUIRES a non-empty `correctable_danxbot_problem_description` naming the problem; false REQUIRES the description be empty. A true+described retro is read by a deterministic, no-LLM check and starts exactly one automated repair (fixes the problem or files a ready card) — the ONLY reliable channel for a danxbot defect found mid-dispatch to get fixed, so do not default to false out of haste.", {
813
811
  id: z.string().min(1),
814
- good: z.string(),
815
- bad: z.string(),
812
+ good: z.string().describe("Free-form markdown (prose or list, they render identically). Style: see issue_comment's `text` param."),
813
+ bad: z.string().describe("Free-form markdown (prose or list, they render identically). Style: see issue_comment's `text` param."),
816
814
  correctable_danxbot_problem: z
817
815
  .boolean()
818
816
  .describe("Required every time, answered honestly — see tool description. Determines whether the description field below must be filled or must be empty."),
@@ -909,7 +907,7 @@ strictTool("plan_list", "List every plan via GET /api/plans (DX-2683), and learn
909
907
  .optional()
910
908
  .describe("Filter to one computed status: awaiting-session, planning, building, complete. Omit for every plan."),
911
909
  }, async (args) => jsonResult(await planList(client, args)));
912
- strictTool("plan_get", "Read a plan via GET /api/plans (DX-2683). Pass `plan_id` to read ANY plan; OMIT to read the plan this session is connected to — omitting while connected to none fails loud `{error: \"session_not_connected\"}`, connect first. A BARE call (no `fields`) returns ONLY the plan's cheap scalars: `{plan, boards, cardCount, bucketCounts, status, session, sessionListenerAttached, available_field_groups}` — no cards, records, or architecture body. `plan` carries `{id, ref, name, createdAt}`; `ref` is the plan's short reference (`PLN-<id>`) — cite that, not the bare id. `status` (DX-2834) is computed fresh on every read, never stored — see `plan_list` for the four values and what each means. Pass `fields` to opt into: `cards` (member cards, PAGED: `cards_offset` (default 0) and `cards_limit` (1.." + LIST_PAGE_MAX_LIMIT + ", default " + PLAN_GET_CARDS_DEFAULT_LIMIT + ") pick the page, in stable card-reference order (board prefix, then card number); response carries `cards_total`/`cards_offset` — page while cards_offset + cards.length < cards_total; either paging arg without `fields: [\"cards\"]` is a 400), `records` (every goal+rule+caveat, keyed by kind) or `records:goal` / `records:rule` / `records:caveat` (one kind, cheaper), `architecture` (`{sections: [{id, planId, contentHash, title, content, sortOrder, createdAt, updatedAt}]}`), `sessions` (every session connected to the plan), `events` (DX-2987/DX-3027 — the plan's durable event ledger: every human action and every bridge message recorded on it, cursor-paged newest first via `events_limit` (1.." + PLAN_GET_EVENTS_MAX_LIMIT + ", default " + PLAN_GET_EVENTS_DEFAULT_LIMIT + ") and `events_before` (an opaque cursor — pass a previous page's `next_cursor` to read older; omit for the newest page); filter with `events_kinds` (one or more, see its own enum), `events_origin` (one, see its own enum), `events_writer` (exact writer name); every `events_*` param without `fields: [\"events\"]` is a 400, same as the `cards_*` params above; response carries `events: {items: [{id, at, kind, writer, origin, originSessionId, targetSessionId, cardId, cardTitle, boardId, detail}], next_cursor}` — `next_cursor` is `null` on the last page; an event on a card whose board you cannot read is left out, plan-level events are always visible). `session`/`sessionListenerAttached` and `available_field_groups` ride every response regardless. `sessionListenerAttached` is `false` for a few seconds right after `plan_connect` while the plugin's event bridge starts; still `false` after that while connected means the plan's card events are not reaching you — tell the operator. ALWAYS `plan_get`/`plan_get_architecture_section` immediately before `plan_update_architecture_section`/`plan_delete_architecture_section` and pass the returned `contentHash` back as `base_hash`.", {
910
+ strictTool("plan_get", "Read a plan via GET /api/plans (DX-2683). Pass `plan_id` to read ANY plan; OMIT to read the plan this session is connected to — omitting while connected to none fails loud `{error: \"session_not_connected\"}`, connect first. A BARE call (no `fields`) returns ONLY the plan's cheap scalars: `{plan, boards, cardCount, bucketCounts, status, session, sessionListenerAttached, available_field_groups}` — no cards, records, or architecture body. `plan` carries `{id, ref, name, createdAt}`; `ref` is the plan's short reference (`PLN-<id>`) — cite that, not the bare id. `status` (DX-2834) is computed fresh on every read, never stored — see `plan_list` for the four values and what each means. Pass `fields` to opt into: `cards` (member cards, PAGED: `cards_offset` (default 0) and `cards_limit` (1.." + LIST_PAGE_MAX_LIMIT + ", default " + PLAN_GET_CARDS_DEFAULT_LIMIT + ") pick the page, in stable card-reference order (board prefix, then card number); response carries `cards_total`/`cards_offset` — page while cards_offset + cards.length < cards_total; either paging arg without `fields: [\"cards\"]` is a 400), `records` (every goal+rule+caveat, keyed by kind) or `records:goal` / `records:rule` / `records:caveat` (one kind, cheaper), `architecture` (`{sections: [{id, planId, contentHash, title, content, sortOrder, createdAt, updatedAt}]}`), `sessions` (every session connected to the plan), `events` (DX-2987/DX-3027 — the plan's durable event ledger, every human action + bridge message, cursor-paged newest first via `events_limit` (1.." + PLAN_GET_EVENTS_MAX_LIMIT + ", default " + PLAN_GET_EVENTS_DEFAULT_LIMIT + ") and `events_before` (a previous page's `next_cursor`; omit for the newest page); filter with `events_kinds` (one+, see its enum), `events_origin` (one, see its enum), `events_writer` (exact name); every `events_*` param without `fields: [\"events\"]` is a 400, same as `cards_*` above; response carries `events: {items: [{id, at, kind, writer, origin, originSessionId, targetSessionId, cardId, cardTitle, boardId, detail}], next_cursor}` — `next_cursor` null on the last page; an event on an unreadable board is left out, plan-level events always visible). `session`/`sessionListenerAttached`/`available_field_groups` ride every response regardless. `sessionListenerAttached` is `false` for a few seconds right after `plan_connect` while the event bridge starts; still `false` after that while connected means plan events aren't reaching you — tell the operator. ALWAYS `plan_get`/`plan_get_architecture_section` immediately before `plan_update_architecture_section`/`plan_delete_architecture_section` and pass the returned `contentHash` back as `base_hash`.", {
913
911
  plan_id: z
914
912
  .number()
915
913
  .int()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.129",
3
+ "version": "0.1.132",
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",