@thehammer/danx-dashboard-mcp 0.1.149 → 0.1.152

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,8 @@ All exposed as `mcp__danx-dashboard__<name>` once wired through the workspace `m
22
22
  | Tool | HTTP | Notes |
23
23
  |---|---|---|
24
24
  | `issue_list` | `GET /api/issues` | filters: `type`, `parent_id` (null → root-only), `dispatchable_derived`, `assigned_agent`, `include_closed`, `limit`, `offset` |
25
- | `issue_get` | `GET /api/issues/:id` or `GET /api/issues/batch` | Pass `id` for one card, or `ids[]` (DX-2727, at most 100) to resolve many across boards in ONE call — global, so `ids` with `board` throws; per-id `not_found` rather than a whole-call 404. Minimal scalars by default; `fields` opts in |
25
+ | `issue_get` | `GET /api/issues/:id` or `GET /api/issues/batch` | Pass `id` for one card, or `ids[]` (DX-2727, at most 100) to resolve many across boards in ONE call — global, so `ids` with `board` throws; per-id `not_found` rather than a whole-call 404. Minimal scalars by default; `fields` (DX-3426) opts in via a recursive field TREE — `{"description": true, "problems": {"solutions": true}, "comments": {"limit": 10}}` — not a flat group list; a cursor-paged relation (today only `comments`) takes its page args (`limit`/`before`) inside its own nested object, so paging now works on the batch form too. See `resource_fields` |
26
+ | `resource_fields` | `GET /api/resources/:resource/fields` | DX-3426 — what an `issue_get`-style field tree may name for one resource: `{resource, description, always, hashes, fields, relations}`. Install-global, no `board`. `resource: "issue"` is the root; a relation's own `resource` in the response is what to call this again with, one level deeper |
26
27
  | `issue_create` | `POST /api/issues` | Epic REQUIRES non-empty `phase_children[]` (atomic insert). `title` = short domain-naming label; `summary` = 1–3 plain-language sentences, always shown; `description` = the collapsed "Context" body. Root and every phase child take their own `summary` |
27
28
  | `issue_edit` | `PATCH /api/issues/:id/edit` | Prose + structured keys (`title`, `summary` (null clears), `description`, `ac`, `checklists`, `effort_level`, `parent_id`, `priority`, `list_id`); semantic keys refused with 400 + pointer to dedicated handler. `priority` (DX-1532) takes a tier word (`low`/`high`/…) or a number in `[0,6)` — the ONLY way to set the numeric column the Trello label + dashboard badge read; never set priority via description prose |
28
29
  | `issue_transition` | `POST /api/issues/:id/transition` | Actions: ready, pickup, rollback_pickup, complete, cancel, block, unblock, archive, reopen. `block` is a dispatch hold only — it never marks the card as needing a human; use `issue_problem` add for that |
@@ -0,0 +1,35 @@
1
+ /**
2
+ * DX-3426 — the shared recursive FIELD-TREE shape.
3
+ *
4
+ * The dashboard's single-card and batch card reads (`issue_get` today;
5
+ * `issue_list` / `plan_get` in later cards, per DX-3426's own follow-up scope)
6
+ * take a nested JSON tree instead of a flat field-GROUP list: each key names a
7
+ * field or relation of the resource at that level, and each value is either
8
+ * `true` (the field, or a relation's child with its default fields), a
9
+ * non-negative integer (a cursor-paged relation's own page ARGUMENT — e.g.
10
+ * `{"comments": {"limit": 10, "before": 812}}`, where `limit`/`before` are
11
+ * themselves plain integer values inside the nested object), or another
12
+ * field tree (a relation's child with a NAMED field selection). Example:
13
+ *
14
+ * {"description": true, "ac": true, "problems": {"solutions": {"steps": true}, "decisions": true}, "comments": {"limit": 10}}
15
+ *
16
+ * One schema, one module — every tool that accepts a field tree imports THIS
17
+ * export rather than redeclaring the shape, so the recursion is defined once
18
+ * and every caller's JSON Schema serializes the same way. `z.lazy` is
19
+ * required because the schema refers to itself; `zod-to-json-schema` (the
20
+ * MCP SDK's own zod v3 → JSON Schema converter, see
21
+ * `@modelcontextprotocol/sdk`'s `zod-json-schema-compat.ts`) resolves that
22
+ * self-reference to a local `$ref` rather than unrolling it infinitely —
23
+ * verified directly against the listed tool schema in
24
+ * `__tests__/tool-defs.test.ts`, not assumed from reading the library's
25
+ * source. `issue_solution`'s pre-existing `steps` field (DX-3310) is the
26
+ * same `z.lazy` self-reference pattern already shipping on this server.
27
+ */
28
+ import { z } from "zod";
29
+ /**
30
+ * The shared schema. A record whose values are `true`, a non-negative
31
+ * integer, or the same schema recursively — generic on purpose (no
32
+ * issue-specific field names baked in here) so `issue_list` / `plan_get` can
33
+ * reuse it unchanged in their own later cards.
34
+ */
35
+ export const fieldTreeSchema = z.lazy(() => z.record(z.union([z.literal(true), z.number().int().nonnegative(), fieldTreeSchema])));
package/dist/handlers.js CHANGED
@@ -95,16 +95,15 @@ export async function issueGet(client, args) {
95
95
  if (args.ids !== undefined && args.board !== undefined) {
96
96
  throw new Error("issue_get: board does not apply to the ids batch form — it resolves every id globally; drop board");
97
97
  }
98
- // DX-3321 — comments paging is a SINGLE-id-route feature
99
- // (`GET /api/issues/:id?comments_limit=&comments_offset=`); the batch
100
- // route has no per-card paging at all. Refuse rather than silently drop,
101
- // same as the `ids` + `board` refusal just above.
102
- if (args.ids !== undefined && (args.comments_limit !== undefined || args.comments_offset !== undefined)) {
103
- throw new Error("issue_get: comments_limit/comments_offset do not apply to the ids batch form — it has no per-card comment paging; drop them or use the single-id form");
104
- }
105
98
  const query = {};
106
- if (args.fields !== undefined && args.fields.length > 0) {
107
- query.fields = args.fields.join(",");
99
+ // DX-3426 — the tree travels as ONE JSON-encoded query param (mirrors how
100
+ // `issue_list`'s `filter`/`sort` envelopes already ride the wire above),
101
+ // rather than the old CSV-of-group-names. Sent on both the single and
102
+ // batch forms — comment paging (or any other relation argument) now lives
103
+ // INSIDE the tree, so there is nothing left for the old ids+comments_*
104
+ // refusal to guard against.
105
+ if (args.fields !== undefined && Object.keys(args.fields).length > 0) {
106
+ query.fields = JSON.stringify(args.fields);
108
107
  }
109
108
  if (args.ids !== undefined) {
110
109
  if (args.ids.length === 0) {
@@ -118,10 +117,6 @@ export async function issueGet(client, args) {
118
117
  if (args.id === undefined) {
119
118
  throw new Error("issue_get: pass exactly one of id or ids");
120
119
  }
121
- if (args.comments_limit !== undefined)
122
- query.comments_limit = args.comments_limit;
123
- if (args.comments_offset !== undefined)
124
- query.comments_offset = args.comments_offset;
125
120
  return client.request({
126
121
  method: "GET",
127
122
  path: `/${encodeURIComponent(args.id)}`,
@@ -1121,6 +1116,10 @@ export const PLAN_EVENT_KINDS = [
1121
1116
  "session_connected",
1122
1117
  "session_switched_away",
1123
1118
  "idle_nudge_sent",
1119
+ // DX-3519 — plan sign-off.
1120
+ "plan_signed_off",
1121
+ "plan_sign_off_cleared",
1122
+ "plan_auto_sign_off_changed",
1124
1123
  ];
1125
1124
  /**
1126
1125
  * DX-3027 — every origin `plan_get`'s `events_origin` filter accepts.
@@ -1130,14 +1129,14 @@ export const PLAN_EVENT_KINDS = [
1130
1129
  */
1131
1130
  export const PLAN_EVENT_ORIGINS = ["operator", "agent", "machine"];
1132
1131
  /**
1133
- * DX-2834 — the plan-status taxonomy, mirroring `PLAN_FIELD_GROUPS` just
1134
- * above: the ONE copy in this package (the `plan_list` zod enum in
1132
+ * DX-2834 / DX-3519 — the plan-status taxonomy, mirroring `PLAN_FIELD_GROUPS`
1133
+ * just above: the ONE copy in this package (the `plan_list` zod enum in
1135
1134
  * `index.ts` reads this const), duplicated from the server's
1136
1135
  * `src/issues/db/plans.ts#PLAN_STATUS_IDS` because the published package
1137
1136
  * cannot import server source at runtime. `__tests__/handlers.test.ts`
1138
1137
  * asserts the two are equal, so drift fails a test rather than a live call.
1139
1138
  */
1140
- export const PLAN_STATUSES = ["awaiting-session", "planning", "building", "complete"];
1139
+ export const PLAN_STATUSES = ["awaiting-session", "planning", "building", "awaiting-sign-off", "complete"];
1141
1140
  /** Every plan, plus which one THIS session is connected to. */
1142
1141
  export async function planList(client, args = {}) {
1143
1142
  return client.request({
@@ -1723,3 +1722,20 @@ export async function dispatchTranscriptSearch(client, args) {
1723
1722
  },
1724
1723
  };
1725
1724
  }
1725
+ // ---------------- resource_fields (DX-3426) ----------------
1726
+ const RESOURCES_BASE_PATH = "/api/resources";
1727
+ /**
1728
+ * `GET /api/resources/:resource/fields` — what a field TREE (`issue_get`'s
1729
+ * `fields`, and any later tool built on the same shape) may name for ONE
1730
+ * resource: `{resource, description, always, hashes, fields, relations}`.
1731
+ * Install-global (no `board` — mirrors `failure_category_list`'s own
1732
+ * board-less rationale just above: nothing server-side would read one).
1733
+ * Unknown resource → 404 `unknown_resource`.
1734
+ */
1735
+ export async function resourceFields(client, args) {
1736
+ return client.request({
1737
+ method: "GET",
1738
+ path: `/${encodeURIComponent(args.resource)}/fields`,
1739
+ basePath: RESOURCES_BASE_PATH,
1740
+ });
1741
+ }
package/dist/index.js CHANGED
@@ -12,7 +12,11 @@
12
12
  * through the workspace `mcp.template.json`):
13
13
  *
14
14
  * - issue_list GET /api/issues
15
- * - issue_get GET /api/issues/:id | /api/issues/batch (DX-2727 ids[] batch)
15
+ * - issue_get GET /api/issues/:id | /api/issues/batch (DX-2727 ids[] batch;
16
+ * DX-3426 `fields` is a recursive field TREE, not a flat group list —
17
+ * see `resource_fields` below and `./field-tree.ts`)
18
+ * - resource_fields GET /api/resources/:resource/fields (DX-3426 — what `fields` may
19
+ * name for a resource; install-global, no `board`)
16
20
  * - issue_create POST /api/issues
17
21
  * - issue_edit PATCH /api/issues/:id/edit
18
22
  * - issue_transition POST /api/issues/:id/transition
@@ -106,8 +110,9 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
106
110
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
107
111
  import { z } from "zod";
108
112
  import { DashboardHttpClient } from "./http-client.js";
109
- import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueProblem, issueQualityGate, issueQualityGateVerdict, issueRetireBranch, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, failureCategoryCreate, failureCategoryList, failureCategoryUpdate, dispatchTranscriptSearch, planAddArchitectureSection, planAddCard, planAddNote, planAddRecord, planConnect, planCreate, planDeleteArchitectureSection, planDeleteNote, planDeleteRecord, planGet, PLAN_FIELD_GROUPS, PLAN_EVENT_KINDS, PLAN_EVENT_ORIGINS, PLAN_STATUSES, ISSUE_BATCH_GET_MAX, LIST_PAGE_MAX_LIMIT, PLAN_GET_CARDS_DEFAULT_LIMIT, PLAN_GET_EVENTS_DEFAULT_LIMIT, PLAN_GET_EVENTS_MAX_LIMIT, planGetArchitectureSection, planGetRecord, planList, planRemoveCard, planRename, planReorderArchitectureSection, planUpdateArchitectureSection, planUpdateNote, planUpdateRecord, repoKnowledgeGet, repoKnowledgeSet, qualityGateInstruction, } from "./handlers.js";
113
+ import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueProblem, issueQualityGate, issueQualityGateVerdict, issueRetireBranch, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, failureCategoryCreate, failureCategoryList, failureCategoryUpdate, dispatchTranscriptSearch, planAddArchitectureSection, planAddCard, planAddNote, planAddRecord, planConnect, planCreate, planDeleteArchitectureSection, planDeleteNote, planDeleteRecord, planGet, PLAN_FIELD_GROUPS, PLAN_EVENT_KINDS, PLAN_EVENT_ORIGINS, PLAN_STATUSES, ISSUE_BATCH_GET_MAX, LIST_PAGE_MAX_LIMIT, PLAN_GET_CARDS_DEFAULT_LIMIT, PLAN_GET_EVENTS_DEFAULT_LIMIT, PLAN_GET_EVENTS_MAX_LIMIT, planGetArchitectureSection, planGetRecord, planList, planRemoveCard, planRename, planReorderArchitectureSection, planUpdateArchitectureSection, planUpdateNote, planUpdateRecord, repoKnowledgeGet, repoKnowledgeSet, qualityGateInstruction, resourceFields, } from "./handlers.js";
110
114
  import { PRIORITY_TIER_WORDS } from "./priority.js";
115
+ import { fieldTreeSchema } from "./field-tree.js";
111
116
  function readEnvOrDie(name) {
112
117
  const v = process.env[name];
113
118
  if (typeof v !== "string" || v === "") {
@@ -272,10 +277,11 @@ const EFFORT_VALUES = [
272
277
  // still be CREATED through this MCP.
273
278
  const ISSUE_TYPES = ["Epic", "Bug", "Feature", "Story", "Chore", "Task"];
274
279
  const NON_EPIC_TYPES = ["Bug", "Feature", "Story", "Chore", "Task"];
275
- // DX-935 / DX-937 — field-group taxonomy for the nested read envelope,
276
- // hand-copied from `src/issues/read/field-groups.ts` (LIST_GROUPS / GET_GROUPS
277
- // — this package cannot import server source). Drift surfaces at runtime as a
278
- // server 400, not silently.
280
+ // DX-935 / DX-937 — field-group taxonomy for the LIST read envelope,
281
+ // hand-copied from `src/issues/read/field-groups.ts` (`LIST_GROUPS` — this
282
+ // package cannot import server source). Drift surfaces at runtime as a server
283
+ // 400, not silently. DX-3426 moved `issue_get` to the field tree
284
+ // (`field-tree.ts`); DX-3427 moves the list the same way and deletes this.
279
285
  const LIST_FIELD_GROUPS = [
280
286
  "description",
281
287
  // DX-2735: replaced the flat "solutions" group (hard cut, no alias).
@@ -290,13 +296,6 @@ const LIST_FIELD_GROUPS = [
290
296
  "children",
291
297
  "effort",
292
298
  ];
293
- const GET_FIELD_GROUPS = [
294
- ...LIST_FIELD_GROUPS,
295
- "mirrors",
296
- "code_review_items",
297
- // DX-2835 — every plan this card is on ({id, ref, name}[]), detail only.
298
- "plans",
299
- ];
300
299
  const SORT_ORDERS = ["asc", "desc"];
301
300
  const sortField = z
302
301
  .array(z.object({
@@ -393,33 +392,27 @@ strictTool("issue_list",
393
392
  }, async (args) => jsonResult(await issueList(client, args)));
394
393
  // ---------------- issue_get ----------------
395
394
  strictTool("issue_get",
396
- // DX-2735: trimmed to pay for the problem tools inside the work-profile
397
- // injected-surface budget — same facts, no repeated prose.
398
- "Fetch one card (`id`) or many (`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 (paged via `comments_limit`/`comments_offset` — see their own field descriptions, single-id form only), 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>`). Within a `comments` page, comments are ordered CHRONOLOGICALLY (oldest first) — only the requested WINDOW is anchored at the newest end, not the array itself; naming `comments_limit`/`comments_offset` alongside `ids` (the batch form) is refused. Single form: unknown id → 404. Batch form: `{issues: [...], not_found: [...ids]}` — an unknown or deleted id never fails the call.", {
395
+ // DX-3426: field GROUPS replaced by a recursive field TREE (see
396
+ // `./field-tree.ts`) — comment paging now lives INSIDE the tree (a
397
+ // cursor-paged relation, today only `comments`, takes `limit`/`before` in
398
+ // its own nested object) instead of top-level `comments_limit`/
399
+ // `comments_offset`, which is also why the old "ids + comments_* refused"
400
+ // rule is gone: paging is per-relation now, so it works on the batch form
401
+ // too. Kept compact — call `resource_fields({resource:"issue"})` for the
402
+ // full, current list of what `fields` may name, rather than enumerating it
403
+ // here (it would drift the moment a field/relation is added or removed).
404
+ "Fetch one card (`id`) or many (`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 THE ALWAYS-ON FIELDS ONLY — identity, lifecycle stamps, derived status/list/blocked, assigned_agent, and content_hash (the concurrency token issue_edit needs for title/description/checklists); every child hash is likewise always returned. `fields` opts into more, per card in a batch too: a JSON object tree — each key names a field or relation, each value is `true` (that field, or a relation's child with its default fields), a nested object (a relation's child with named fields), or on the cursor-paged `comments` relation the nested object may also carry `limit`/`before` (comment id) to page — e.g. `{\"description\": true, \"problems\": {\"solutions\": {\"steps\": true}}, \"comments\": {\"limit\": 10}}`; a paged response also carries `comments_page: {limit, total, next_cursor}` (`next_cursor` → pass back as `before` for the next older page, null = none). Call `resource_fields({resource:\"issue\"})` for exactly what may be requested (every field/relation name, each relation's own child resource, and its paging shape if any) — never guess a name. Unknown field/relation → 400 `unknown_field`; bad shape → 400 `invalid_fields`; bad `limit`/`before` → 400 `invalid_field_argument`; a field you lack permission for → 403 `forbidden_field`. Single form: unknown id → 404. Batch form: `{issues: [...], not_found: [...ids]}` — an unknown or deleted id never fails the call.", {
399
405
  id: z.string().min(1).optional(),
400
406
  ids: z.array(z.string().min(1)).min(1).max(ISSUE_BATCH_GET_MAX).optional(),
401
- fields: z
402
- .array(z.enum(GET_FIELD_GROUPS))
403
- .optional()
404
- .describe("Field groups to add; absent = minimal scalars."),
405
- comments_limit: z
406
- .number()
407
- .int()
408
- .positive()
409
- .optional()
410
- .describe("Single-id form only. How many comments the page holds (server default 20), the window anchored at the " +
411
- "NEWEST comment. An out-of-range value (server max 200) is refused by the server with its own 400, " +
412
- "never silently clamped here. Requires `fields:[\"comments\"]`."),
413
- comments_offset: z
414
- .number()
415
- .int()
416
- .nonnegative()
407
+ fields: fieldTreeSchema
417
408
  .optional()
418
- .describe("Single-id form only. Skip this many of the newest comments before the page starts (default 0) — page " +
419
- "forward while comments_offset + comments.length < comments_total to reach older comments. Requires " +
420
- "`fields` to include `comments`."),
409
+ .describe("A field tree (see tool description); absent/empty = minimal scalars. `resource_fields({resource:\"issue\"})` names every valid key."),
421
410
  ...boardField,
422
411
  }, async (args) => jsonResult(await issueGet(client, args)));
412
+ // ---------------- resource_fields ----------------
413
+ strictTool("resource_fields", "DX-3426: what a field TREE (`issue_get`'s `fields`, and any later tool that takes one) may name for one resource — `{resource, description, always: string[], hashes: string[], fields: [{name, description}], relations: [{name, description, resource, paging?: {default_limit, max_limit, page_field}}]}`. `always`/`hashes` ride every response regardless of `fields` (never request them). A relation's `resource` names what to call this same tool with next, to go one level deeper. Install-global, not board-scoped. `resource: \"issue\"` is the root; unknown resource → 404 `unknown_resource`.", {
414
+ resource: z.string().min(1).describe('Resource name, e.g. "issue", or a relation\'s own `resource` from a prior call.'),
415
+ }, async (args) => jsonResult(await resourceFields(client, args)));
423
416
  // ---------------- issue_create ----------------
424
417
  strictTool("issue_create", '`plan` is REQUIRED on every create — no default, no inference; see its own field description for the two values and the refusal. This replaces the plan_add_card follow-up at creation time; plan_add_card remains for putting an EXISTING card on a plan. ' +
425
418
  'Create a card. 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` adds gates beyond the board\'s type defaults (see its own field description); add one later with `issue_quality_gate`. ALWAYS pass `triage_enabled` explicitly on the root card and every phase child (see its own field description).', {
@@ -948,11 +941,11 @@ strictTool("brief_set_page", 'Write one Brief page. Board-scoped; see `board`. B
948
941
  // plan id — and it can only ever bind the caller's own session. `plan_create`
949
942
  // also takes no plan id, but for a different reason: it MAKES a plan rather
950
943
  // than acting on one, so there is no existing plan for an id to name yet.
951
- strictTool("plan_list", "List every plan, and learn which plan THIS session is connected to. Plans are GLOBAL, not board-scoped — a plan is a named, dated set of cards an operator assembled by hand, from any repo. Returns `{plans: [{id, ref, name, createdAt, cardCount, boards, status}], session, sessionListenerAttached}` — `ref` is the plan's short reference (`PLN-<id>`), cite it rather than the bare id. `status` is computed fresh on every read, never stored: `awaiting-session` (no live session — a session row isn't released just because a session ended), `planning` (no cards, or only Review/Backlog/terminal ones with ≥1 not Done/Cancelled), `building` (a live session AND ≥1 card ToDo/In Progress or stuck-but-active — Blocked/Needs Help), `complete` (≥1 card, all Done/Cancelled — wins even with no session). Pass `status` to filter. `session` is your own registration (`{sessionId, title, planId, planName, firstSeenAt, lastActiveAt}`) or null outside a Claude Code session; `planId: null` means connected to no plan — `plan_get` to browse, `plan_connect` to bind. `sessionListenerAttached` says whether your event stream is attached (the plugin's plan bridge); false for a few seconds right after connect is normal, false after that means events aren't reaching you — tell the operator. Distinct from the board Brief (`brief_list`).", {
944
+ strictTool("plan_list", "List every plan, and learn which plan THIS session is connected to. Plans are GLOBAL, not board-scoped — a plan is a named, dated set of cards an operator assembled by hand, from any repo. Returns `{plans: [{id, ref, name, createdAt, cardCount, boards, status, signedOffAt, signedOffBy, autoSignOff}], session, sessionListenerAttached}` — `ref` is the plan's short reference (`PLN-<id>`), cite it rather than the bare id. `status` is computed fresh on every read, never stored — except the sign-off pair itself: `awaiting-session` (no live session — a session row isn't released just because a session ended), `planning` (no cards, or only Review/Backlog/terminal ones with ≥1 not Done/Cancelled), `building` (a live session AND ≥1 card ToDo/In Progress or stuck-but-active — Blocked/Needs Help), `awaiting-sign-off` (≥1 card, all Done/Cancelled, but nobody has signed the plan off yet — DX-3519), `complete` (≥1 card, all Done/Cancelled, AND signed off — wins even with no session). `signedOffAt`/`signedOffBy` are `null` until a human (`signedOffBy` = their identity) or the `autoSignOff` toggle (`signedOffBy: 'auto'`) signs the plan off; sign-off itself is a human-only write, not exposed through this MCP surface — direct the operator to the dashboard's plan page. Pass `status` to filter. `session` is your own registration (`{sessionId, title, planId, planName, firstSeenAt, lastActiveAt}`) or null outside a Claude Code session; `planId: null` means connected to no plan — `plan_get` to browse, `plan_connect` to bind. `sessionListenerAttached` says whether your event stream is attached (the plugin's plan bridge); false for a few seconds right after connect is normal, false after that means events aren't reaching you — tell the operator. Distinct from the board Brief (`brief_list`).", {
952
945
  status: z
953
946
  .enum(PLAN_STATUSES)
954
947
  .optional()
955
- .describe("Filter to one computed status: awaiting-session, planning, building, complete. Omit for every plan."),
948
+ .describe("Filter to one computed status: awaiting-session, planning, building, awaiting-sign-off, complete. Omit for every plan."),
956
949
  }, async (args) => jsonResult(await planList(client, args)));
957
950
  strictTool("plan_get", "Read a plan. 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` 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 via `cards_offset`/`cards_limit` — see their own field descriptions — in stable card-reference order: board prefix then card number, pages never repeat/skip unless membership changes between reads; response carries `cards_total`), `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` (the plan's durable event ledger — every human action and bridge message, paged/filtered via `events_limit`/`events_before`/`events_kinds`/`events_origin`/`events_writer` — see their own field descriptions; 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 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`.", {
958
951
  plan_id: z
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.149",
3
+ "version": "0.1.152",
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",