@thehammer/danx-dashboard-mcp 0.1.131 → 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.
- package/dist/handlers.js +61 -0
- package/dist/index.js +80 -93
- package/package.json +1 -1
package/dist/handlers.js
CHANGED
|
@@ -413,6 +413,39 @@ function argCheckers(tool, mode) {
|
|
|
413
413
|
},
|
|
414
414
|
};
|
|
415
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* Every action-dispatched tool's args carry these three regardless of
|
|
418
|
+
* `action` — never action-specific, so every per-action allow-list passed to
|
|
419
|
+
* `refuseInapplicableFields` is unioned with this rather than repeating it.
|
|
420
|
+
*/
|
|
421
|
+
const STRUCTURAL_ACTION_FIELDS = ["id", "action", "board"];
|
|
422
|
+
/**
|
|
423
|
+
* DX-3310 (code-review nit) — a field the caller sent that means nothing for
|
|
424
|
+
* the chosen `action` used to be silently ignored: `issue_solution`'s `add`
|
|
425
|
+
* built its `content` object from `title`/`body`/`pro`/`con`/`recommended`
|
|
426
|
+
* regardless of action, then simply never read it on `remove`/`add_step`/etc,
|
|
427
|
+
* and `issue_problem`'s `remove` never looked at `statement`/`context`/
|
|
428
|
+
* `solutions` at all. Either way the caller got a 200 with the field quietly
|
|
429
|
+
* discarded — no different from a typo. Refused loud instead, mirroring
|
|
430
|
+
* `argCheckers`' own wording: every ACTUALLY-SUPPLIED (`!== undefined`) key
|
|
431
|
+
* outside the action's own allow-list (plus `STRUCTURAL_ACTION_FIELDS`,
|
|
432
|
+
* always allowed) is named in one error, so a caller fixes every offending
|
|
433
|
+
* field in one round trip rather than one at a time.
|
|
434
|
+
*/
|
|
435
|
+
// A9 (code-review fix) — takes `object` rather than `Record<string, unknown>`
|
|
436
|
+
// so every caller (each with its own specific `*Args` interface, no index
|
|
437
|
+
// signature) passes `args` directly, with no `as unknown as Record<...>`
|
|
438
|
+
// bridge cast at the call site. `Object.entries` reads the key/value pairs
|
|
439
|
+
// without needing an index signature on the input type either.
|
|
440
|
+
function refuseInapplicableFields(tool, mode, args, allowed) {
|
|
441
|
+
const offending = Object.entries(args)
|
|
442
|
+
.filter(([key, value]) => value !== undefined && !STRUCTURAL_ACTION_FIELDS.includes(key) && !allowed.has(key))
|
|
443
|
+
.map(([key]) => key)
|
|
444
|
+
.sort();
|
|
445
|
+
if (offending.length > 0) {
|
|
446
|
+
throw new Error(`${tool} ${mode} refuses fields that do not apply to this action: ${offending.join(", ")}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
416
449
|
export async function issueComment(client, args) {
|
|
417
450
|
const idEnc = encodeURIComponent(args.id);
|
|
418
451
|
const board = args.board;
|
|
@@ -614,10 +647,25 @@ export async function issueChecklist(client, args) {
|
|
|
614
647
|
*
|
|
615
648
|
* No answer action, for the reason `issueSolution` gives.
|
|
616
649
|
*/
|
|
650
|
+
/**
|
|
651
|
+
* DX-3310 (nit) — per-action allow-list for `refuseInapplicableFields`.
|
|
652
|
+
* `id`/`action`/`board` are structural and never listed (every action takes
|
|
653
|
+
* them). `type` is listed under `edit` even though it can never actually be
|
|
654
|
+
* SENT there (see the dedicated throw below) — that keeps the generic
|
|
655
|
+
* refusal from firing first with a less specific message than the one that
|
|
656
|
+
* already names the real reason.
|
|
657
|
+
*/
|
|
658
|
+
const PROBLEM_ACTION_FIELDS = {
|
|
659
|
+
list: [],
|
|
660
|
+
add: ["statement", "context", "type", "summary", "solutions"],
|
|
661
|
+
edit: ["problem_id", "base_hash", "statement", "context", "summary", "type"],
|
|
662
|
+
remove: ["problem_id", "base_hash"],
|
|
663
|
+
};
|
|
617
664
|
export async function issueProblem(client, args) {
|
|
618
665
|
const idEnc = encodeURIComponent(args.id);
|
|
619
666
|
const board = args.board;
|
|
620
667
|
const need = argCheckers("issue_problem", `action=${args.action}`);
|
|
668
|
+
refuseInapplicableFields("issue_problem", `action=${args.action}`, args, new Set(PROBLEM_ACTION_FIELDS[args.action]));
|
|
621
669
|
switch (args.action) {
|
|
622
670
|
case "list":
|
|
623
671
|
return client.request({ method: "GET", path: `/${idEnc}/problems`, board });
|
|
@@ -693,11 +741,24 @@ export async function issueProblem(client, args) {
|
|
|
693
741
|
* question could release the very stop it set to wait for a human. The operator
|
|
694
742
|
* answers in the dashboard.
|
|
695
743
|
*/
|
|
744
|
+
const SOLUTION_ACTION_FIELDS = {
|
|
745
|
+
add: ["title", "body", "pro", "con", "recommended", "steps"],
|
|
746
|
+
edit: ["solution_id", "base_hash", "title", "body", "pro", "con", "recommended", "steps", "steps_base_hash"],
|
|
747
|
+
remove: ["solution_id", "base_hash"],
|
|
748
|
+
add_step: ["solution_id", "title", "description", "parent_step_id", "position"],
|
|
749
|
+
edit_step: ["solution_id", "step_id", "base_hash", "title", "description"],
|
|
750
|
+
remove_step: ["solution_id", "step_id", "base_hash"],
|
|
751
|
+
};
|
|
696
752
|
export async function issueSolution(client, args) {
|
|
697
753
|
const need = argCheckers("issue_solution", `action=${args.action}`);
|
|
698
754
|
// DX-2735: checked at runtime too, not only by the schema — a caller that skips
|
|
699
755
|
// the MCP boundary must never build `/problems/undefined/solutions`.
|
|
700
756
|
const problemId = need.id(args.problem_id, "problem_id");
|
|
757
|
+
// DX-3310 (nit) — `problem_id` is unioned in here rather than listed in
|
|
758
|
+
// every `SOLUTION_ACTION_FIELDS` entry: unlike `issue_problem` (where it
|
|
759
|
+
// only applies to edit/remove), `issue_solution` requires it for every
|
|
760
|
+
// action (checked above), so it is structural FOR THIS TOOL specifically.
|
|
761
|
+
refuseInapplicableFields("issue_solution", `action=${args.action}`, args, new Set(["problem_id", ...SOLUTION_ACTION_FIELDS[args.action]]));
|
|
701
762
|
const base = `/${encodeURIComponent(args.id)}/problems/${problemId}/solutions`;
|
|
702
763
|
const board = args.board;
|
|
703
764
|
const content = {};
|
package/dist/index.js
CHANGED
|
@@ -338,14 +338,14 @@ const boardField = {
|
|
|
338
338
|
.string()
|
|
339
339
|
.min(1)
|
|
340
340
|
.optional()
|
|
341
|
-
.describe("
|
|
341
|
+
.describe("Board id `<repo>:<slug>`; omit for this dispatch's board. Unknown → 404."),
|
|
342
342
|
};
|
|
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
|
|
347
|
-
const SUMMARY_DESCRIBE = "1–3 plain-language sentences, no markdown/jargon
|
|
348
|
-
const DESCRIPTION_DESCRIBE = 'The full body ("Context"): evidence, examples, technical detail; markdown, collapsed by default.
|
|
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
349
|
// DX-3335 — condensed from the now-deleted danxbot:comment-style skill (PLN-11
|
|
350
350
|
// R-16: fold a skill's rule into the tool description it governs rather than
|
|
351
351
|
// require a separate load). Shared by every prose-writing field below so an
|
|
@@ -355,7 +355,7 @@ const MARKDOWN_STYLE_DESCRIBE = "Renders as markdown here. Use `##`/`###` header
|
|
|
355
355
|
strictTool("issue_list",
|
|
356
356
|
// DX-2735: trimmed to pay for the problem tools inside the work-profile
|
|
357
357
|
// injected-surface budget — same facts, no repeated prose.
|
|
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
|
|
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.", {
|
|
359
359
|
filter: z
|
|
360
360
|
.object({
|
|
361
361
|
q: z.string().optional(),
|
|
@@ -389,7 +389,7 @@ strictTool("issue_list",
|
|
|
389
389
|
strictTool("issue_get",
|
|
390
390
|
// DX-2735: trimmed to pay for the problem tools inside the work-profile
|
|
391
391
|
// injected-surface budget — same facts, no repeated prose.
|
|
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
|
|
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.", {
|
|
393
393
|
id: z.string().min(1).optional(),
|
|
394
394
|
ids: z.array(z.string().min(1)).min(1).max(ISSUE_BATCH_GET_MAX).optional(),
|
|
395
395
|
fields: z
|
|
@@ -403,7 +403,7 @@ strictTool("issue_get",
|
|
|
403
403
|
.optional()
|
|
404
404
|
.describe("Single-id form only. How many comments the page holds (server default 20), the window anchored at the " +
|
|
405
405
|
"NEWEST comment. An out-of-range value (server max 200) is refused by the server with its own 400, " +
|
|
406
|
-
"never silently clamped here. Requires `fields
|
|
406
|
+
"never silently clamped here. Requires `fields:[\"comments\"]`."),
|
|
407
407
|
comments_offset: z
|
|
408
408
|
.number()
|
|
409
409
|
.int()
|
|
@@ -415,8 +415,8 @@ strictTool("issue_get",
|
|
|
415
415
|
...boardField,
|
|
416
416
|
}, async (args) => jsonResult(await issueGet(client, args)));
|
|
417
417
|
// ---------------- issue_create ----------------
|
|
418
|
-
strictTool("issue_create", '`plan`
|
|
419
|
-
'Create a card via POST /api/issues. Board-scoped; see `board`. type=Epic REQUIRES non-empty phase_children[] (epic
|
|
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.', {
|
|
420
420
|
type: z.enum(ISSUE_TYPES),
|
|
421
421
|
title: z.string().min(1).describe(TITLE_DESCRIBE),
|
|
422
422
|
summary: z.string().min(1).optional().describe(SUMMARY_DESCRIBE),
|
|
@@ -428,7 +428,7 @@ strictTool("issue_create", '`plan` is REQUIRED on every create (DX-3006): pass "
|
|
|
428
428
|
plan: z
|
|
429
429
|
.literal("mine")
|
|
430
430
|
.nullable()
|
|
431
|
-
.describe('
|
|
431
|
+
.describe('REQUIRED, decide per card — see tool description. "mine" = this session\'s connected plan; null = deliberately none; no plan id accepted.'),
|
|
432
432
|
parent_id: z.string().nullable().optional(),
|
|
433
433
|
ac: z.array(z.object({ title: z.string().min(1) }).strict()).optional(),
|
|
434
434
|
effort_level: z.enum(EFFORT_VALUES).nullable().optional(),
|
|
@@ -479,7 +479,7 @@ strictTool("issue_create", '`plan` is REQUIRED on every create (DX-3006): pass "
|
|
|
479
479
|
priority: z
|
|
480
480
|
.union([z.enum(PRIORITY_TIER_WORDS), z.number()])
|
|
481
481
|
.optional()
|
|
482
|
-
.describe('
|
|
482
|
+
.describe('Tier word ("lowest"…"critical", prefer this) or number in [0,6); higher = more urgent. Omit for the route\'s own default.'),
|
|
483
483
|
// DX-3238 — `assigned_agent` is DELIBERATELY left undeclared here, unlike
|
|
484
484
|
// `priority` above. The route accepts it (`ALLOWED_CREATE_KEYS`,
|
|
485
485
|
// create.ts:39-56) but it only matters for the create→`list_id`-lands-
|
|
@@ -495,7 +495,7 @@ strictTool("issue_create", '`plan` is REQUIRED on every create (DX-3006): pass "
|
|
|
495
495
|
...boardField,
|
|
496
496
|
}, async (args) => jsonResult(await issueCreate(client, args, config.board)));
|
|
497
497
|
// ---------------- issue_edit ----------------
|
|
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
|
|
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.', {
|
|
499
499
|
id: z.string().min(1),
|
|
500
500
|
title: z.string().min(1).optional().describe(TITLE_DESCRIBE),
|
|
501
501
|
summary: z
|
|
@@ -516,7 +516,7 @@ strictTool("issue_edit", 'Patch a card via PATCH /api/issues/:id/edit. ALLOWED k
|
|
|
516
516
|
status: z
|
|
517
517
|
.enum(CHECKLIST_ITEM_STATUSES)
|
|
518
518
|
.optional()
|
|
519
|
-
.describe("Optional full status; omitted → from `checked`.
|
|
519
|
+
.describe("Optional full status; omitted → from `checked`. See tool description for the deferred/📡 rules."),
|
|
520
520
|
detail: z
|
|
521
521
|
.string()
|
|
522
522
|
.optional()
|
|
@@ -543,7 +543,7 @@ strictTool("issue_edit", 'Patch a card via PATCH /api/issues/:id/edit. ALLOWED k
|
|
|
543
543
|
priority: z
|
|
544
544
|
.union([z.enum(PRIORITY_TIER_WORDS), z.number()])
|
|
545
545
|
.optional()
|
|
546
|
-
.describe('
|
|
546
|
+
.describe('Tier word ("lowest"…"critical", prefer this) or number in [0,6); higher = more urgent — see tool description for why this is the only way to set it.'),
|
|
547
547
|
list_id: z.string().min(1).nullable().optional(),
|
|
548
548
|
triage_enabled: z
|
|
549
549
|
.boolean()
|
|
@@ -602,7 +602,7 @@ const CHECKLIST_ITEM_INPUT = z
|
|
|
602
602
|
status: z.enum(CHECKLIST_ITEM_STATUSES).optional(),
|
|
603
603
|
})
|
|
604
604
|
.strict();
|
|
605
|
-
strictTool("issue_checklist", "Targeted checklist CUD via /api/issues/:id/checklists[/:cid[/items[/:iid]]] (DX-1362). Mutates ONE checklist/item without the wholesale `issue_edit({checklists})` replace, which DROPS any checklist you omit and churns every item id (orphaning its Trello mirror) — prefer this for single-item changes. Action-dispatched: add_list (POST :id/checklists {name, items?}) — create a named checklist, optionally with initial items; update_list (PATCH :id/checklists/:cid {name}) — rename; remove_list (DELETE :id/checklists/:cid) — soft-delete the checklist (audit trail preserved); add_item (POST :id/checklists/:cid/items {label, detail?, status?}) — append an item (status defaults `incomplete`); update_item (PATCH :id/checklists/:cid/items/:iid {label?, detail?, status?}) — change ONLY the fields you pass, in place (keeps id + Trello link; ≥1 field required); remove_item (DELETE :id/checklists/:cid/items/:iid) — soft-delete one item. Status: incomplete|failing|passing|cancelled|deferred (terminal = passing|cancelled|deferred;
|
|
605
|
+
strictTool("issue_checklist", "Targeted checklist CUD via /api/issues/:id/checklists[/:cid[/items[/:iid]]] (DX-1362). Mutates ONE checklist/item without the wholesale `issue_edit({checklists})` replace, which DROPS any checklist you omit and churns every item id (orphaning its Trello mirror) — prefer this for single-item changes. Action-dispatched: add_list (POST :id/checklists {name, items?}) — create a named checklist, optionally with initial items; update_list (PATCH :id/checklists/:cid {name}) — rename; remove_list (DELETE :id/checklists/:cid) — soft-delete the checklist (audit trail preserved); add_item (POST :id/checklists/:cid/items {label, detail?, status?}) — append an item (status defaults `incomplete`); update_item (PATCH :id/checklists/:cid/items/:iid {label?, detail?, status?}) — change ONLY the fields you pass, in place (keeps id + Trello link; ≥1 field required); remove_item (DELETE :id/checklists/:cid/items/:iid) — soft-delete one item. Status: incomplete|failing|passing|cancelled|deferred (terminal = passing|cancelled|deferred; `deferred` = work done but a real-world/post-deploy check still outstanding, REQUIRES a non-empty `detail`). checklist_id is required for every action except add_list; item_id for update_item/remove_item. Each returns the {ok,status,body} envelope; unknown card/checklist/item → 404, invalid status → 400. ADDITIVE — `issue_edit({checklists})` still works for bulk authoring.", {
|
|
606
606
|
id: z.string().min(1),
|
|
607
607
|
action: z.enum([
|
|
608
608
|
"add_list",
|
|
@@ -638,22 +638,20 @@ const STEP_INPUT = z.lazy(() => z
|
|
|
638
638
|
.int()
|
|
639
639
|
.positive()
|
|
640
640
|
.optional()
|
|
641
|
-
.describe("present ->
|
|
642
|
-
"
|
|
643
|
-
"
|
|
644
|
-
"
|
|
645
|
-
"
|
|
646
|
-
"progress, a later single-step edit)."),
|
|
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."),
|
|
647
646
|
title: z
|
|
648
647
|
.string()
|
|
649
648
|
.min(1)
|
|
650
|
-
.describe("one imperative line
|
|
651
|
-
"in the title itself."),
|
|
649
|
+
.describe("one imperative line — labels (\"1.\", \"2a\") are DERIVED on read, never put one in the title."),
|
|
652
650
|
description: z.string().nullable().optional().describe("markdown detail, or null/omitted for none"),
|
|
653
651
|
steps: z.array(STEP_INPUT).optional().describe("this step's own children, nested — refused past 3 levels total"),
|
|
654
652
|
})
|
|
655
653
|
.strict());
|
|
656
|
-
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 —
|
|
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`.", {
|
|
657
655
|
id: z.string().min(1),
|
|
658
656
|
action: z.enum(["list", "add", "edit", "remove"]),
|
|
659
657
|
problem_id: z.number().int().positive().optional().describe("edit/remove"),
|
|
@@ -662,9 +660,7 @@ strictTool("issue_problem", "A card's PROBLEMS via /api/issues/:id/problems[/:pi
|
|
|
662
660
|
.string()
|
|
663
661
|
.min(1)
|
|
664
662
|
.optional()
|
|
665
|
-
.describe("add/edit; ONE plain sentence, at most 200 characters
|
|
666
|
-
"WHAT IS TO BE DONE — the deed itself, never phrased as a question (\"Rotate the staging DB credential\", " +
|
|
667
|
-
"not \"Should we rotate it?\")."),
|
|
663
|
+
.describe("add/edit; ONE plain sentence, at most 200 characters — see `type` below for question vs action phrasing."),
|
|
668
664
|
context: z
|
|
669
665
|
.string()
|
|
670
666
|
.nullable()
|
|
@@ -676,67 +672,58 @@ strictTool("issue_problem", "A card's PROBLEMS via /api/issues/:id/problems[/:pi
|
|
|
676
672
|
type: z
|
|
677
673
|
.enum(["question", "action"])
|
|
678
674
|
.optional()
|
|
679
|
-
.describe("add only.
|
|
680
|
-
"
|
|
681
|
-
"
|
|
682
|
-
"
|
|
683
|
-
"
|
|
684
|
-
"
|
|
685
|
-
"
|
|
686
|
-
"
|
|
687
|
-
"
|
|
688
|
-
"person needs to carry it out; solutions[] are the possible ROUTES a person could take (e.g. \"rotate by " +
|
|
689
|
-
"hand in the console\" vs \"run the provisioning script\" vs \"ask the vendor\"), and EACH ONE MUST CARRY " +
|
|
690
|
-
"AT LEAST ONE STEP (refused 400 otherwise) — an action with no procedure is not yet actionable. Omitted " +
|
|
691
|
-
"defaults to \"question\" — 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."),
|
|
692
684
|
summary: z
|
|
693
685
|
.string()
|
|
694
686
|
.nullable()
|
|
695
687
|
.optional()
|
|
696
|
-
.describe("add/edit. Plain text, 1-3 sentences — never markdown
|
|
697
|
-
"
|
|
698
|
-
"
|
|
699
|
-
"
|
|
700
|
-
"when given (omit/null on a question means none). edit: omit to keep the stored summary, null to clear it " +
|
|
701
|
-
"(refused if the problem is a live action)."),
|
|
688
|
+
.describe("add/edit. Plain text, 1-3 sentences — never markdown (that's `context`'s job). Required+non-empty for an " +
|
|
689
|
+
"action problem (why needed + why you can't do it yourself — see `type`; refused 400 if missing on add or " +
|
|
690
|
+
"cleared on a live action edit); optional for a question. add: sent only when given (omit/null on a " +
|
|
691
|
+
"question means none). edit: omit keeps the stored summary, null clears it."),
|
|
702
692
|
solutions: z
|
|
703
693
|
.array(z.object({ title: z.string().min(1), ...SOLUTION_FIELDS, steps: z.array(STEP_INPUT).optional() }).strict())
|
|
704
694
|
.optional()
|
|
705
|
-
.describe("add only; fields as issue_solution add, INCLUDING steps
|
|
695
|
+
.describe("add only; fields as issue_solution add, INCLUDING steps — an ACTION problem's inline " +
|
|
706
696
|
"solutions must each carry at least one step (refused 400 otherwise); a QUESTION's may carry any number, " +
|
|
707
697
|
"including none."),
|
|
708
698
|
...boardField,
|
|
709
699
|
}, async (args) => jsonResult(await issueProblem(client, args)));
|
|
710
700
|
// ---------------- issue_solution ----------------
|
|
711
|
-
strictTool("issue_solution", "One problem's options via /api/issues/:id/problems/:pid/solutions[/:sid],
|
|
712
|
-
"procedure steps via .../solutions/:sid/steps[/:stepId]; `problem_id`
|
|
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; " +
|
|
713
703
|
"another problem's solution id → 404). add {title, body?, pro?, con?, recommended?, steps?}: title names the " +
|
|
714
|
-
"option
|
|
715
|
-
"
|
|
716
|
-
"
|
|
717
|
-
"(
|
|
718
|
-
"
|
|
719
|
-
"
|
|
720
|
-
"
|
|
721
|
-
"
|
|
722
|
-
"
|
|
723
|
-
"
|
|
724
|
-
"`
|
|
725
|
-
"
|
|
726
|
-
"
|
|
727
|
-
"
|
|
728
|
-
"
|
|
729
|
-
"
|
|
730
|
-
"
|
|
731
|
-
"
|
|
732
|
-
"
|
|
733
|
-
"
|
|
734
|
-
"live children
|
|
735
|
-
"
|
|
736
|
-
"
|
|
737
|
-
"base_hash} — soft-deletes it AND its own live children; refused (409) if this solution belongs to an ACTION " +
|
|
738
|
-
"problem and removing it would leave zero live steps. A stale base_hash on a step → 409 `stale_step` with " +
|
|
739
|
-
"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`).", {
|
|
740
727
|
id: z.string().min(1),
|
|
741
728
|
action: z.enum(["add", "edit", "remove", "add_step", "edit_step", "remove_step"]),
|
|
742
729
|
problem_id: z.number().int().positive(),
|
|
@@ -768,7 +755,7 @@ strictTool("issue_solution", "One problem's options via /api/issues/:id/problems
|
|
|
768
755
|
...boardField,
|
|
769
756
|
}, async (args) => jsonResult(await issueSolution(client, args)));
|
|
770
757
|
// ---------------- issue_dependency ----------------
|
|
771
|
-
strictTool("issue_dependency", 'Dependency CRUD via /api/issues/:id/dependencies[/:did]. action=add → POST {kind, target_id, reason} where kind ∈ {depends_on, conflict_on}. depends_on adds are CYCLE-CHECKED (BFS from target back to source — 409 if loop). Idempotent: re-adding a live triple returns the existing id. Self-loops refuse 409. action=remove → DELETE /:did. The server REQUIRES the literal reason="recorded_in_error" on removal (encodes "removal means NOT related, never satisfied")
|
|
758
|
+
strictTool("issue_dependency", 'Dependency CRUD via /api/issues/:id/dependencies[/:did]. action=add → POST {kind, target_id, reason} where kind ∈ {depends_on, conflict_on}. depends_on adds are CYCLE-CHECKED (BFS from target back to source — 409 if loop). Idempotent: re-adding a live triple returns the existing id. Self-loops refuse 409. action=remove → DELETE /:did. The server REQUIRES the literal reason="recorded_in_error" on removal (encodes "removal means NOT related, never satisfied"); this MCP boundary hardcodes it, so callers do not pass reason on remove. This is the ONLY mechanism the dispatch picker enforces to sequence one card after another — leaving a card at Review/held (e.g. an issue_triage "keep" verdict) is NOT a substitute.', {
|
|
772
759
|
id: z.string().min(1),
|
|
773
760
|
action: z.enum(["add", "remove"]),
|
|
774
761
|
kind: z.enum(["depends_on", "conflict_on"]).optional(),
|
|
@@ -805,7 +792,7 @@ strictTool("issue_quality_gate", "Put one quality gate ON a card, or take it OFF
|
|
|
805
792
|
...boardField,
|
|
806
793
|
}, async (args) => jsonResult(await issueQualityGate(client, args)));
|
|
807
794
|
// ---------------- issue_quality_gate_verdict ----------------
|
|
808
|
-
strictTool("issue_quality_gate_verdict", "Stamp an operator MANUAL quality-gate VERDICT via PATCH /api/issues/:id/quality-gates/:gate {status, message} — the same write the dashboard Gates-tab Pass / Fail / Revert controls perform (DX-1373). SIBLING of `issue_quality_gate`, not a replacement: that one puts a gate ON or OFF the card (does this gate apply at all), THIS one records the VERDICT (did it pass) — POST vs PATCH on the same resource, neither substitutes for the other. **Use this to close out a card you picked up with `issue_transition pickup {manual:true}
|
|
795
|
+
strictTool("issue_quality_gate_verdict", "Stamp an operator MANUAL quality-gate VERDICT via PATCH /api/issues/:id/quality-gates/:gate {status, message} — the same write the dashboard Gates-tab Pass / Fail / Revert controls perform (DX-1373). SIBLING of `issue_quality_gate`, not a replacement: that one puts a gate ON or OFF the card (does this gate apply at all), THIS one records the VERDICT (did it pass) — POST vs PATCH on the same resource, neither substitutes for the other. **Use this to close out a card you picked up with `issue_transition pickup {manual:true}`**: `issue_transition complete` REFUSES 409 (`failed_gate: \"quality_gate_post\"`, `failed_post_gates[]`) while any required POST gate is not `pass` — without a verdict a manually-claimed card strands In Progress, stalling other cards via its `conflict_on`/`waiting_on` edges. `status`: `pass` | `fail` | `pending` (revert a prior verdict, clears the message). `message` is the accountability record for the override, REQUIRED at >= 20 characters for `pass`/`fail` (shorter → 400), ignored for `pending`. Record the REAL reviewer finding, not a rubber stamp. A manual verdict is a PURE row write: no side effects — a manual `fail` never blocks the card and a manual `pass` never releases a dispatch. Unknown gate → 400; bad status → 400; unknown card → 404. Board-scoped; see `board`.", {
|
|
809
796
|
id: z.string().min(1),
|
|
810
797
|
gate: z.enum([
|
|
811
798
|
"plan-dependency",
|
|
@@ -820,13 +807,13 @@ strictTool("issue_quality_gate_verdict", "Stamp an operator MANUAL quality-gate
|
|
|
820
807
|
...boardField,
|
|
821
808
|
}, async (args) => jsonResult(await issueQualityGateVerdict(client, args)));
|
|
822
809
|
// ---------------- issue_retro ----------------
|
|
823
|
-
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,
|
|
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.", {
|
|
824
811
|
id: z.string().min(1),
|
|
825
|
-
good: z.string().describe("Free-form markdown (prose or
|
|
826
|
-
bad: z.string().describe("Free-form markdown (prose or
|
|
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."),
|
|
827
814
|
correctable_danxbot_problem: z
|
|
828
815
|
.boolean()
|
|
829
|
-
.describe("Required
|
|
816
|
+
.describe("Required every time, answered honestly — see tool description. Determines whether the description field below must be filled or must be empty."),
|
|
830
817
|
correctable_danxbot_problem_description: z
|
|
831
818
|
.string()
|
|
832
819
|
.describe("Required. Non-empty (naming the concrete problem) when correctable_danxbot_problem is true; must be empty when it is false."),
|
|
@@ -920,7 +907,7 @@ strictTool("plan_list", "List every plan via GET /api/plans (DX-2683), and learn
|
|
|
920
907
|
.optional()
|
|
921
908
|
.describe("Filter to one computed status: awaiting-session, planning, building, complete. Omit for every plan."),
|
|
922
909
|
}, async (args) => jsonResult(await planList(client, args)));
|
|
923
|
-
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
|
|
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`.", {
|
|
924
911
|
plan_id: z
|
|
925
912
|
.number()
|
|
926
913
|
.int()
|
|
@@ -936,39 +923,39 @@ strictTool("plan_get", "Read a plan via GET /api/plans (DX-2683). Pass `plan_id`
|
|
|
936
923
|
.int()
|
|
937
924
|
.nonnegative()
|
|
938
925
|
.optional()
|
|
939
|
-
.describe("Where the `cards` page starts (default 0). Requires `fields
|
|
926
|
+
.describe("Where the `cards` page starts (default 0). Requires `fields:[\"cards\"]`. Page with cards_offset while cards_offset + cards.length < cards_total."),
|
|
940
927
|
cards_limit: z
|
|
941
928
|
.number()
|
|
942
929
|
.int()
|
|
943
930
|
.positive()
|
|
944
931
|
.max(LIST_PAGE_MAX_LIMIT)
|
|
945
932
|
.optional()
|
|
946
|
-
.describe("How many cards one page holds, 1.." + LIST_PAGE_MAX_LIMIT + " (default " + PLAN_GET_CARDS_DEFAULT_LIMIT + "). Requires `fields
|
|
933
|
+
.describe("How many cards one page holds, 1.." + LIST_PAGE_MAX_LIMIT + " (default " + PLAN_GET_CARDS_DEFAULT_LIMIT + "). Requires `fields:[\"cards\"]`."),
|
|
947
934
|
events_limit: z
|
|
948
935
|
.number()
|
|
949
936
|
.int()
|
|
950
937
|
.positive()
|
|
951
938
|
.max(PLAN_GET_EVENTS_MAX_LIMIT)
|
|
952
939
|
.optional()
|
|
953
|
-
.describe("DX-3027 — how many ledger events one page holds, 1.." + PLAN_GET_EVENTS_MAX_LIMIT + " (default " + PLAN_GET_EVENTS_DEFAULT_LIMIT + "). Requires `fields
|
|
940
|
+
.describe("DX-3027 — how many ledger events one page holds, 1.." + PLAN_GET_EVENTS_MAX_LIMIT + " (default " + PLAN_GET_EVENTS_DEFAULT_LIMIT + "). Requires `fields:[\"events\"]`."),
|
|
954
941
|
events_before: z
|
|
955
942
|
.string()
|
|
956
943
|
.min(1)
|
|
957
944
|
.optional()
|
|
958
|
-
.describe("
|
|
945
|
+
.describe("an opaque cursor from a previous page's `next_cursor`. Omit for the newest page. Requires `fields:[\"events\"]`."),
|
|
959
946
|
events_kinds: z
|
|
960
947
|
.array(z.enum(PLAN_EVENT_KINDS))
|
|
961
948
|
.optional()
|
|
962
|
-
.describe("
|
|
949
|
+
.describe("only these event kinds. Omit for every kind. Requires `fields:[\"events\"]`."),
|
|
963
950
|
events_origin: z
|
|
964
951
|
.enum(PLAN_EVENT_ORIGINS)
|
|
965
952
|
.optional()
|
|
966
|
-
.describe("
|
|
953
|
+
.describe("only events with this origin. Omit for every origin. Requires `fields:[\"events\"]`."),
|
|
967
954
|
events_writer: z
|
|
968
955
|
.string()
|
|
969
956
|
.min(1)
|
|
970
957
|
.optional()
|
|
971
|
-
.describe("
|
|
958
|
+
.describe("only events with this exact writer name. Omit for every writer. Requires `fields:[\"events\"]`."),
|
|
972
959
|
}, async (args) => jsonResult(await planGet(client, args)));
|
|
973
960
|
strictTool("plan_create", "Create a new, empty plan via POST /api/plans (DX-2531). GLOBAL — a plan is not board-scoped, and this call adds no cards, no records, and no architecture sections; it does NOT connect any session to the new plan (call `plan_connect` separately, exactly as adding a card to a plan is its own separate step). Returns `{ok, status, body: {plan: {id, ref, name, createdAt}}}` — `ref` is the plan's short reference (`PLN-<id>`). Use the returned `plan.id` with `plan_connect` to start working on it, or with `plan_get({plan_id})` to browse it.", {
|
|
974
961
|
name: z.string().min(1).describe("The plan's name — shown in the Plans list."),
|
|
@@ -1038,9 +1025,9 @@ strictTool("plan_update_note", 'Edit a plan note, via PATCH /api/plans/:plan_id/
|
|
|
1038
1025
|
content_hash: z.string().describe("The note's `contentHash` from your last read. Required."),
|
|
1039
1026
|
title: z.string().min(1).optional().describe("New title, at most 60 characters. Omit to keep the stored title."),
|
|
1040
1027
|
body: z.string().min(1).optional().describe("New wrap-up, at most 250 characters. Omit to keep the stored body."),
|
|
1041
|
-
card_ids: z.array(z.string().min(1)).optional().describe("
|
|
1042
|
-
record_refs: z.array(z.string().min(1)).optional().describe("
|
|
1043
|
-
section_ids: z.array(z.number().int().positive()).optional().describe("
|
|
1028
|
+
card_ids: z.array(z.string().min(1)).optional().describe("Part of the link-set group — see tool description."),
|
|
1029
|
+
record_refs: z.array(z.string().min(1)).optional().describe("Part of the link-set group — see tool description."),
|
|
1030
|
+
section_ids: z.array(z.number().int().positive()).optional().describe("Part of the link-set group — see tool description."),
|
|
1044
1031
|
}, async (args) => jsonResult(await planUpdateNote(client, args)));
|
|
1045
1032
|
strictTool("plan_delete_note", 'Soft-delete a plan note, via DELETE /api/plans/:plan_id/notes/:note_id (DX-2915). `content_hash` must be the note\'s `contentHash` from your last read; a stale hash deletes nothing and returns `{error: "stale_plan_note", currentHash, currentTitle, currentBody, currentLinks}` — the same shape `plan_update_note` uses. TAKES AN EXPLICIT `plan_id`, same reason as `plan_add_note`. Unknown plan, unknown note, or an already-deleted note → 404. DX-3072 — returns `{notes_count}`, the plan\'s remaining live note count, not the latest page.', {
|
|
1046
1033
|
plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
|
|
@@ -1125,7 +1112,7 @@ strictTool("failure_category_update", "Patch an existing failure category via PA
|
|
|
1125
1112
|
expectedRate: expectedRateField,
|
|
1126
1113
|
}, async (args) => jsonResult(await failureCategoryUpdate(client, args)));
|
|
1127
1114
|
// ---------------- dispatch_transcript_search (DX-3221) ----------------
|
|
1128
|
-
strictTool("dispatch_transcript_search", "Search or tail ANOTHER dispatch's stored JSONL session transcript via
|
|
1115
|
+
strictTool("dispatch_transcript_search", "Search or tail ANOTHER dispatch's stored JSONL session transcript via GET /api/dispatches/:id/logs (DX-1682/DX-1484/DX-3221) — no new server route, only in-process search/windowing over the transcript already captured there. Prefer this over reading the transcript file directly: no filesystem access needed, and a single JSONL entry can be far longer than a line-based file read can sub-page — this tool searches the string itself and returns only a bounded excerpt AROUND a match, never the whole line. With `pattern`: returns up to `maxMatches` matching lines (case-insensitive regex), each a windowed excerpt of up to `contextChars` characters centered on the first match. Without `pattern`: returns the most recent `tail` lines instead, each capped at `contextChars`. `truncated: true` marks a capped excerpt either way, never a thrown error. `dispatchId` is a real `dispatches.id` — exactly the ids a failure-repair card's own body already lists under \"dispatches that hit it\".", {
|
|
1129
1116
|
dispatchId: z.string().min(1).describe("The dispatch id whose transcript to search — from a failure-repair card's own body, or any other dispatch id you already have."),
|
|
1130
1117
|
pattern: z.string().min(1).optional().describe("Case-insensitive regex tested against each raw JSONL line. Omit to get a tail read of the most recent lines instead."),
|
|
1131
1118
|
tail: z.number().int().positive().optional().describe("Only used when `pattern` is omitted. How many of the most recent lines to return. Defaults to 20, capped at 200."),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thehammer/danx-dashboard-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.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",
|