@thehammer/danx-dashboard-mcp 0.1.150 → 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/dist/handlers.js CHANGED
@@ -1116,6 +1116,10 @@ export const PLAN_EVENT_KINDS = [
1116
1116
  "session_connected",
1117
1117
  "session_switched_away",
1118
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",
1119
1123
  ];
1120
1124
  /**
1121
1125
  * DX-3027 — every origin `plan_get`'s `events_origin` filter accepts.
@@ -1125,14 +1129,14 @@ export const PLAN_EVENT_KINDS = [
1125
1129
  */
1126
1130
  export const PLAN_EVENT_ORIGINS = ["operator", "agent", "machine"];
1127
1131
  /**
1128
- * DX-2834 — the plan-status taxonomy, mirroring `PLAN_FIELD_GROUPS` just
1129
- * 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
1130
1134
  * `index.ts` reads this const), duplicated from the server's
1131
1135
  * `src/issues/db/plans.ts#PLAN_STATUS_IDS` because the published package
1132
1136
  * cannot import server source at runtime. `__tests__/handlers.test.ts`
1133
1137
  * asserts the two are equal, so drift fails a test rather than a live call.
1134
1138
  */
1135
- export const PLAN_STATUSES = ["awaiting-session", "planning", "building", "complete"];
1139
+ export const PLAN_STATUSES = ["awaiting-session", "planning", "building", "awaiting-sign-off", "complete"];
1136
1140
  /** Every plan, plus which one THIS session is connected to. */
1137
1141
  export async function planList(client, args = {}) {
1138
1142
  return client.request({
package/dist/index.js CHANGED
@@ -941,11 +941,11 @@ strictTool("brief_set_page", 'Write one Brief page. Board-scoped; see `board`. B
941
941
  // plan id — and it can only ever bind the caller's own session. `plan_create`
942
942
  // also takes no plan id, but for a different reason: it MAKES a plan rather
943
943
  // than acting on one, so there is no existing plan for an id to name yet.
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}], 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`).", {
945
945
  status: z
946
946
  .enum(PLAN_STATUSES)
947
947
  .optional()
948
- .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."),
949
949
  }, async (args) => jsonResult(await planList(client, args)));
950
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`.", {
951
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.150",
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",
@@ -1,43 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
- import { mkdtempSync, writeFileSync, symlinkSync, rmSync, realpathSync, } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { pathToFileURL } from "node:url";
6
- import { isEntrypointModule } from "./entrypoint.js";
7
- describe("isEntrypointModule (DX-1647)", () => {
8
- let dir;
9
- let real;
10
- let link;
11
- beforeAll(() => {
12
- dir = mkdtempSync(join(tmpdir(), "entrypoint-test-"));
13
- real = join(dir, "index.js");
14
- writeFileSync(real, "// stub entry\n");
15
- link = join(dir, "danx-dashboard-mcp"); // mimics node_modules/.bin symlink
16
- symlinkSync(real, link);
17
- });
18
- afterAll(() => {
19
- rmSync(dir, { recursive: true, force: true });
20
- });
21
- // import.meta.url always reports the module REALPATH — model that here.
22
- const moduleUrl = () => pathToFileURL(realpathSync(real)).href;
23
- it("true when argv[1] is the real file (direct `node index.js`)", () => {
24
- expect(isEntrypointModule(moduleUrl(), real)).toBe(true);
25
- });
26
- it("true when argv[1] is a SYMLINK to the file (npx / global bin) — the fleet regression", () => {
27
- // The symlink path !== the realpath, but the entrypoint MUST still be
28
- // detected, or npx imports the module and exits without booting the server.
29
- expect(link).not.toBe(realpathSync(link));
30
- expect(isEntrypointModule(moduleUrl(), link)).toBe(true);
31
- });
32
- it("false when argv[1] is undefined (module imported, not run as bin)", () => {
33
- expect(isEntrypointModule(moduleUrl(), undefined)).toBe(false);
34
- });
35
- it("false when argv[1] is an empty string", () => {
36
- expect(isEntrypointModule(moduleUrl(), "")).toBe(false);
37
- });
38
- it("false when argv[1] points at a different real file", () => {
39
- const other = join(dir, "other.js");
40
- writeFileSync(other, "// other\n");
41
- expect(isEntrypointModule(moduleUrl(), other)).toBe(false);
42
- });
43
- });