@thehammer/danx-dashboard-mcp 0.1.162 → 0.1.164
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 +10 -32
- package/dist/entrypoint.test.js +43 -0
- package/dist/event-text.js +137 -0
- package/dist/field-tree.js +5 -4
- package/dist/handlers.js +166 -1534
- package/dist/index.js +175 -819
- package/dist/lifecycle.js +447 -0
- package/dist/priority.js +4 -4
- package/package.json +1 -1
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-3465 (PLN-11 R-25 / plan rule R-25) — the "lifecycle" tool set: agent-
|
|
3
|
+
* specific dispatch callbacks, merged from danxbot's own `src/mcp/danxbot-server.ts`
|
|
4
|
+
* (DX-3459 problem 1754, decision: one server, named tool sets, switched on
|
|
5
|
+
* per profile). These are NOT dashboard CRUD — they are the agent's own
|
|
6
|
+
* self-report channel to the worker/dashboard that spawned it: "I'm done"
|
|
7
|
+
* (`danxbot_complete`), Slack reply/update/recovery, quality-gate sign-off,
|
|
8
|
+
* template-app load/save, and the evaluator summary. Every dashboard CRUD
|
|
9
|
+
* operation these tools used to sit beside (issue_*, plan_* etc.) goes
|
|
10
|
+
* through `danxbot_api` instead (R-25) — see `index.ts`.
|
|
11
|
+
*
|
|
12
|
+
* Registered UNCONDITIONALLY at module load (so the DX-1606 tool-defs
|
|
13
|
+
* generator — which never calls `boot()` — still introspects every one of
|
|
14
|
+
* them for token-cost accounting), then individually `.disable()`d inside
|
|
15
|
+
* `boot()` (`index.ts`) based on which per-dispatch callback URL env vars
|
|
16
|
+
* are actually present — the same visibility contract
|
|
17
|
+
* `buildActiveTools`/`callTool` used to enforce in the old standalone
|
|
18
|
+
* server: a tool whose URL wasn't injected for this dispatch is not just
|
|
19
|
+
* inert, it is invisible to `tools/list`.
|
|
20
|
+
*
|
|
21
|
+
* DX-3394 problem 1682 cleanup folded into this merge:
|
|
22
|
+
* - `danxbot_restart_worker` — already removed on `main` before this card
|
|
23
|
+
* (DX-3231 host-mode removal); never carried over.
|
|
24
|
+
* - `quality_gate_skip` folded into `quality_gate_complete` as
|
|
25
|
+
* `status: "skipped"` (reason ≥20 chars, same server-side bar).
|
|
26
|
+
* - `slack_replace_message` + `slack_purge_message` merged into
|
|
27
|
+
* `slack_recovery_message({action: "replace" | "purge", ts, text?})`.
|
|
28
|
+
* - `ready` / `archive` / `review` removed from `AGENT_COMPLETE_STATUSES`
|
|
29
|
+
* (Core Principle 1 — no legacy aliases). The `danx-flesh-out` plugin
|
|
30
|
+
* skill instructed the old aliases; that is Part B (plugin-side prose),
|
|
31
|
+
* not reachable from this repo.
|
|
32
|
+
* - `danxbot_update_router_prompt` DELETED OUTRIGHT (DX-3459 problem 1755,
|
|
33
|
+
* decision 251: the router prompt becomes an ordinary board setting,
|
|
34
|
+
* reached through `danxbot_api` with a scoped permission — no dedicated
|
|
35
|
+
* tool). The slack-agent profile documents the route (Part B).
|
|
36
|
+
* - Descriptions trimmed of DX-card historical narration where cheap to do
|
|
37
|
+
* without re-deriving the byte-for-byte target DX-3394 measured.
|
|
38
|
+
*/
|
|
39
|
+
import { z } from "zod";
|
|
40
|
+
/**
|
|
41
|
+
* The six canonical agent-visible `danxbot_complete` statuses. DX-3394
|
|
42
|
+
* problem 1682 / Core Principle 1 — the pre-DX-770 deprecated aliases
|
|
43
|
+
* (`completed`, `agent_blocked`) were already gone; this merge additionally
|
|
44
|
+
* drops the `ready` / `archive` / `review` backward-compat aliases that
|
|
45
|
+
* Core Principle 1 forbids as standing legacy tolerance. A dispatch that
|
|
46
|
+
* needs one of those non-terminal lifecycle stamps transitions the card
|
|
47
|
+
* FIRST through `danxbot_api` ({method:'POST', path:'/api/issues/:id/transition',
|
|
48
|
+
* body:{action:'ready'|'archive'}}), then calls
|
|
49
|
+
* `danxbot_complete({status:'complete'})`.
|
|
50
|
+
*/
|
|
51
|
+
export const AGENT_COMPLETE_STATUSES = [
|
|
52
|
+
"complete",
|
|
53
|
+
"cancelled",
|
|
54
|
+
"failed",
|
|
55
|
+
"critical_failure",
|
|
56
|
+
"waiting_on_dependency",
|
|
57
|
+
];
|
|
58
|
+
/** DX-722 — minimum `summary` length for `failed` to count as a legitimate self-block. */
|
|
59
|
+
export const ADEQUATE_SUMMARY_MIN_CHARS = 30;
|
|
60
|
+
/** DX-1369 — minimum reason length for a `quality_gate_complete` "skipped" verdict. */
|
|
61
|
+
export const QUALITY_GATE_SKIP_REASON_MIN_CHARS = 20;
|
|
62
|
+
export function hasAdequateBlockSummary(summary) {
|
|
63
|
+
return typeof summary === "string" && summary.trim().length >= ADEQUATE_SUMMARY_MIN_CHARS;
|
|
64
|
+
}
|
|
65
|
+
/** Read the lifecycle callback URLs straight from `process.env`. */
|
|
66
|
+
export function readLifecycleUrlsFromEnv() {
|
|
67
|
+
const opt = (name) => {
|
|
68
|
+
const v = process.env[name];
|
|
69
|
+
return typeof v === "string" && v !== "" ? v : undefined;
|
|
70
|
+
};
|
|
71
|
+
return {
|
|
72
|
+
stop: opt("DANXBOT_STOP_URL"),
|
|
73
|
+
slackReply: opt("DANXBOT_SLACK_REPLY_URL"),
|
|
74
|
+
slackUpdate: opt("DANXBOT_SLACK_UPDATE_URL"),
|
|
75
|
+
linkThreadToIssue: opt("DANXBOT_LINK_THREAD_TO_ISSUE_URL"),
|
|
76
|
+
slackRecoveryList: opt("DANXBOT_SLACK_RECOVERY_LIST_URL"),
|
|
77
|
+
slackRecoveryMessage: opt("DANXBOT_SLACK_RECOVERY_MESSAGE_URL"),
|
|
78
|
+
slackRecoveryCreateOrLink: opt("DANXBOT_SLACK_RECOVERY_CREATE_OR_LINK_URL"),
|
|
79
|
+
qualityGate: opt("DANXBOT_QUALITY_GATE_URL"),
|
|
80
|
+
evaluatorSummary: opt("DANXBOT_EVALUATOR_SUMMARY_URL"),
|
|
81
|
+
saveTemplateApp: opt("DANXBOT_TEMPLATE_APP_SAVE_URL"),
|
|
82
|
+
loadTemplateApp: opt("DANXBOT_TEMPLATE_APP_LOAD_URL"),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async function postJson(url, body, bearer) {
|
|
86
|
+
return fetch(url, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: {
|
|
89
|
+
"Content-Type": "application/json",
|
|
90
|
+
...(bearer ? { Authorization: `Bearer ${bearer}` } : {}),
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify(body),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/** DX-925 — bearer for dispatch-internal WORKER routes (stop, quality-gate, template-app, ...). */
|
|
96
|
+
function agentBearer() {
|
|
97
|
+
return process.env.DANX_AGENT_TOKEN;
|
|
98
|
+
}
|
|
99
|
+
/** DX-2246 — bearer for the callbacks that target the DASHBOARD (slack reply/update, link-thread, slack-recovery). */
|
|
100
|
+
function dashboardBearer() {
|
|
101
|
+
return process.env.DANXBOT_DASHBOARD_TOKEN;
|
|
102
|
+
}
|
|
103
|
+
async function postWorkerRoute(url, body, toolName, bearer) {
|
|
104
|
+
const response = await postJson(url, body, bearer);
|
|
105
|
+
const text = await response.text();
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
throw new Error(`${toolName} worker endpoint returned HTTP ${response.status}: ${text}`);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
return JSON.parse(text);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return { raw: text };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function parseOptionalAbsolutePathArray(field, value) {
|
|
117
|
+
if (value === undefined)
|
|
118
|
+
return [];
|
|
119
|
+
return value.map((entry, idx) => {
|
|
120
|
+
if (!entry.startsWith("/")) {
|
|
121
|
+
throw new Error(`field "${field}"[${idx}] must be an absolute path (got "${entry}")`);
|
|
122
|
+
}
|
|
123
|
+
return entry;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Registers every lifecycle tool on `server` via the caller's `strictTool`
|
|
128
|
+
* wrapper (so every tool here gets the same DX-3238 strict-shape guard the
|
|
129
|
+
* rest of this package's tools use) and returns the `RegisteredTool` handles
|
|
130
|
+
* for the ones that are conditionally visible — `boot()` disables each based
|
|
131
|
+
* on `readLifecycleUrlsFromEnv()`.
|
|
132
|
+
*/
|
|
133
|
+
export function registerLifecycleTools(strictTool) {
|
|
134
|
+
// ---------------- danxbot_complete ----------------
|
|
135
|
+
// Always visible — every dispatch, of every kind, ends with this call.
|
|
136
|
+
strictTool("danxbot_complete", "Finalize the current dispatch. Releases the worker's per-dispatch scope and ends the dispatch row. " +
|
|
137
|
+
"Does NOT move any card — if you're working an issue-bound dispatch and want the card to move " +
|
|
138
|
+
"(Done / Blocked / Cancelled / etc.), transition the card FIRST through danxbot_api " +
|
|
139
|
+
"({method:'POST', path:'/api/issues/:id/transition', body:{action:...}}), then call this tool. " +
|
|
140
|
+
"Always call this when done instead of simply stopping output.", {
|
|
141
|
+
status: z.enum(AGENT_COMPLETE_STATUSES).describe("complete = work shipped, ends the dispatch row (does NOT move the card — transition it first); " +
|
|
142
|
+
"failed = the dispatch could not finish its job; ends the dispatch row failed + a strike; see " +
|
|
143
|
+
`summary for its ≥${ADEQUATE_SUMMARY_MIN_CHARS}-char requirement. Does NOT block the card — if the ` +
|
|
144
|
+
"cause is card-specific, first transition the card with action 'block' and a reason through " +
|
|
145
|
+
"danxbot_api ({method:'POST', path:'/api/issues/:id/transition', body:{action:'block', reason}}). " +
|
|
146
|
+
"Waiting on other work is a depends_on edge, not a block; when a human is 100% required, add a " +
|
|
147
|
+
"problem with solutions instead; " +
|
|
148
|
+
"cancelled = agent voluntarily abandons the dispatch (does NOT cancel the card — transition it first); " +
|
|
149
|
+
"critical_failure = environment broken in a way that affects every dispatch (MCP tools not loading, " +
|
|
150
|
+
"Bash unavailable, Claude auth missing) — halts the poller until a human clears it; " +
|
|
151
|
+
"waiting_on_dependency = genuinely blocked on a card that must land on another board/repo — file that " +
|
|
152
|
+
"change as a card on the right board, add a depends_on edge from yours to it, commit your own WIP, " +
|
|
153
|
+
"transition yours rollback_pickup, then end with this status (never complete/ready/archive/review, " +
|
|
154
|
+
"which delete your branch)."),
|
|
155
|
+
summary: z
|
|
156
|
+
.string()
|
|
157
|
+
.describe("Brief summary of what happened. For failed, the dispatch-environment failure reason, " +
|
|
158
|
+
`≥${ADEQUATE_SUMMARY_MIN_CHARS} chars or the call downgrades to cancelled + a strike. For ` +
|
|
159
|
+
"critical_failure, the specific environment issue. For complete, a one-line summary of what shipped. " +
|
|
160
|
+
"Card-side context belongs on the transition call (through danxbot_api), not here."),
|
|
161
|
+
}, async (args) => {
|
|
162
|
+
const stopUrl = readLifecycleUrlsFromEnv().stop;
|
|
163
|
+
if (!stopUrl) {
|
|
164
|
+
throw new Error("danxbot_complete: DANXBOT_STOP_URL not configured for this dispatch");
|
|
165
|
+
}
|
|
166
|
+
let response;
|
|
167
|
+
try {
|
|
168
|
+
response = await postJson(stopUrl, { status: args.status, summary: args.summary }, agentBearer());
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
172
|
+
throw new Error(`danxbot_complete: POST to worker stop endpoint failed (${detail}) — terminal transition NOT applied.`);
|
|
173
|
+
}
|
|
174
|
+
if (!response.ok) {
|
|
175
|
+
const text = await response.text();
|
|
176
|
+
throw new Error(`danxbot_complete: worker stop endpoint returned HTTP ${response.status}: ${text} — terminal transition NOT applied.`);
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
content: [{ type: "text", text: `Agent signaled ${args.status}: ${args.summary}` }],
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
// ---------------- danxbot_slack_reply ----------------
|
|
183
|
+
const slackReply = strictTool("danxbot_slack_reply", "Post the FINAL user-facing reply to the originating Slack thread. Call exactly once per dispatch, " +
|
|
184
|
+
"immediately before danxbot_complete. Optionally attach result files via files[] (absolute local paths " +
|
|
185
|
+
"on the shared filesystem) — text + attachments land as ONE atomic thread post. For intermediate " +
|
|
186
|
+
"progress use danxbot_slack_post_update instead.", {
|
|
187
|
+
text: z.string().describe("The final reply text to post to the Slack thread."),
|
|
188
|
+
files: z
|
|
189
|
+
.array(z.string())
|
|
190
|
+
.optional()
|
|
191
|
+
.describe("Optional absolute local paths to files to attach to the reply."),
|
|
192
|
+
}, async (args) => {
|
|
193
|
+
const url = readLifecycleUrlsFromEnv().slackReply;
|
|
194
|
+
if (!url)
|
|
195
|
+
throw new Error("danxbot_slack_reply called outside a Slack dispatch");
|
|
196
|
+
const files = parseOptionalAbsolutePathArray("files", args.files);
|
|
197
|
+
const body = { text: args.text };
|
|
198
|
+
if (files.length > 0)
|
|
199
|
+
body.files = files;
|
|
200
|
+
const response = await postJson(url, body, dashboardBearer());
|
|
201
|
+
if (!response.ok)
|
|
202
|
+
throw new Error(`Slack reply API returned HTTP ${response.status}`);
|
|
203
|
+
return {
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
type: "text",
|
|
207
|
+
text: files.length > 0
|
|
208
|
+
? `Reply posted to Slack thread with ${files.length} file(s)`
|
|
209
|
+
: "Reply posted to Slack thread",
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
// ---------------- danxbot_slack_post_update ----------------
|
|
215
|
+
const slackPostUpdate = strictTool("danxbot_slack_post_update", "Post an intermediate progress update to the originating Slack thread. Use SPARINGLY — only for " +
|
|
216
|
+
"meaningful status the user cares about. Do NOT post for every file read or trivial step.", { text: z.string().describe("The intermediate status update text.") }, async (args) => {
|
|
217
|
+
const url = readLifecycleUrlsFromEnv().slackUpdate;
|
|
218
|
+
if (!url)
|
|
219
|
+
throw new Error("danxbot_slack_post_update called outside a Slack dispatch");
|
|
220
|
+
const response = await postJson(url, { text: args.text }, dashboardBearer());
|
|
221
|
+
if (!response.ok)
|
|
222
|
+
throw new Error(`Slack update API returned HTTP ${response.status}`);
|
|
223
|
+
return { content: [{ type: "text", text: "Update posted to Slack thread" }] };
|
|
224
|
+
});
|
|
225
|
+
// ---------------- link_thread_to_issue ----------------
|
|
226
|
+
const linkThreadToIssue = strictTool("link_thread_to_issue", "Upgrade THIS Slack thread into a tracked card: bind the thread to ONE issue (immutable 1:1) and " +
|
|
227
|
+
"register the Slack mirror so future card updates re-render the thread's card-view message. Thread " +
|
|
228
|
+
"scope is IMPLICIT (channel + thread_ts come from the dispatch) — pass only the issue id. Call this " +
|
|
229
|
+
"AFTER posting your substantive answer with danxbot_slack_reply (that reply becomes the card-view " +
|
|
230
|
+
'"message #1"). A second call on an already-linked thread fails loud with a 409 — never a silent re-link.', { issue_id: z.string().describe('The card id to bind this thread to (e.g. "DX-1086").') }, async (args) => {
|
|
231
|
+
const url = readLifecycleUrlsFromEnv().linkThreadToIssue;
|
|
232
|
+
if (!url)
|
|
233
|
+
throw new Error("link_thread_to_issue called outside a Slack dispatch");
|
|
234
|
+
const result = await postWorkerRoute(url, { issue_id: args.issue_id }, "link_thread_to_issue", dashboardBearer());
|
|
235
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
236
|
+
});
|
|
237
|
+
// ---------------- slack_recovery_list ----------------
|
|
238
|
+
const slackRecoveryList = strictTool("slack_recovery_list", "Read-only. Returns the Slack thread's full stored message history plus failures[], the recorded " +
|
|
239
|
+
"router failures projected out of those same messages, so you can reason about what happened before " +
|
|
240
|
+
"mutating anything. Only available on a slack-recovery dispatch.", {}, async () => {
|
|
241
|
+
const url = readLifecycleUrlsFromEnv().slackRecoveryList;
|
|
242
|
+
if (!url)
|
|
243
|
+
throw new Error("slack_recovery_list called outside a slack-recovery dispatch");
|
|
244
|
+
const result = await postWorkerRoute(url, {}, "slack_recovery_list", dashboardBearer());
|
|
245
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
246
|
+
});
|
|
247
|
+
// ---------------- slack_recovery_message (merged replace+purge, DX-3394 problem 1682) ----------------
|
|
248
|
+
const slackRecoveryMessage = strictTool("slack_recovery_message", "Replace or delete ONE message you posted in this thread. `action: 'replace'` (`chat.update`) rewrites " +
|
|
249
|
+
"the message text — the replaced message becomes this thread's card-view anchor ('message #1'): a " +
|
|
250
|
+
"later link_thread_to_issue registers THIS ts, and danxbot re-renders the real card view into this " +
|
|
251
|
+
"exact slot, so write a short human line, never a hand-written card view, in `text`. `action: 'purge'` " +
|
|
252
|
+
"(`chat.delete` + prunes the matching thread-history entry in ONE call — never post-and-hope these " +
|
|
253
|
+
"happen separately). Both REFUSE any ts you did not author (server-side) and are idempotent: replacing " +
|
|
254
|
+
"or purging an already-gone ts succeeds as a no-op.", {
|
|
255
|
+
action: z.enum(["replace", "purge"]),
|
|
256
|
+
ts: z
|
|
257
|
+
.string()
|
|
258
|
+
.describe("The Slack message timestamp to act on. Must be a message this bot authored (from " +
|
|
259
|
+
"slack_recovery_list's messages[] with isBot:true — every failures[] postedTs is one)."),
|
|
260
|
+
text: z
|
|
261
|
+
.string()
|
|
262
|
+
.optional()
|
|
263
|
+
.describe("Required for action:'replace' — the new message text. Ignored for 'purge'."),
|
|
264
|
+
}, async (args) => {
|
|
265
|
+
const url = readLifecycleUrlsFromEnv().slackRecoveryMessage;
|
|
266
|
+
if (!url)
|
|
267
|
+
throw new Error("slack_recovery_message called outside a slack-recovery dispatch");
|
|
268
|
+
if (args.action === "replace" && args.text === undefined) {
|
|
269
|
+
throw new Error("slack_recovery_message: text is required for action:'replace'");
|
|
270
|
+
}
|
|
271
|
+
const body = { action: args.action, ts: args.ts };
|
|
272
|
+
if (args.text !== undefined)
|
|
273
|
+
body.text = args.text;
|
|
274
|
+
const result = await postWorkerRoute(url, body, "slack_recovery_message", dashboardBearer());
|
|
275
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
276
|
+
});
|
|
277
|
+
// ---------------- slack_recovery_create_or_link ----------------
|
|
278
|
+
const slackRecoveryCreateOrLink = strictTool("slack_recovery_create_or_link", "The ONLY way to get a card id for this thread on a slack-recovery dispatch (danxbot_api's card-create " +
|
|
279
|
+
"route, POST /api/issues, is not available here). Resolves SERVER-SIDE whether the thread already " +
|
|
280
|
+
"resolves to a card before creating " +
|
|
281
|
+
"anything: a hit returns that existing card's id (created:false) and creates NOTHING; a miss creates a " +
|
|
282
|
+
"fresh card with the fields you pass (created:true) — a duplicate card is structurally impossible. " +
|
|
283
|
+
"Either way, follow up with link_thread_to_issue({issue_id}) using the returned id.", {
|
|
284
|
+
type: z
|
|
285
|
+
.enum(["Bug", "Story", "Chore"])
|
|
286
|
+
.describe("The card type. Never Epic/Feature — a single Slack request never warrants a container card."),
|
|
287
|
+
title: z.string().describe("Card title."),
|
|
288
|
+
description: z
|
|
289
|
+
.string()
|
|
290
|
+
.describe("Card description — sourced from the triggering human message's content."),
|
|
291
|
+
ac: z
|
|
292
|
+
.array(z.object({ title: z.string() }).strict())
|
|
293
|
+
.optional()
|
|
294
|
+
.describe("Optional acceptance-criteria items."),
|
|
295
|
+
effort_level: z.string().optional().describe("Optional effort level."),
|
|
296
|
+
priority: z.number().optional().describe("Optional priority in [0, 6)."),
|
|
297
|
+
triage_enabled: z.boolean().optional().describe("Optional auto-triage opt-in; absent → false."),
|
|
298
|
+
}, async (args) => {
|
|
299
|
+
const url = readLifecycleUrlsFromEnv().slackRecoveryCreateOrLink;
|
|
300
|
+
if (!url)
|
|
301
|
+
throw new Error("slack_recovery_create_or_link called outside a slack-recovery dispatch");
|
|
302
|
+
const body = {
|
|
303
|
+
type: args.type,
|
|
304
|
+
title: args.title,
|
|
305
|
+
description: args.description,
|
|
306
|
+
};
|
|
307
|
+
if (args.ac !== undefined)
|
|
308
|
+
body.ac = args.ac;
|
|
309
|
+
if (args.effort_level !== undefined)
|
|
310
|
+
body.effort_level = args.effort_level;
|
|
311
|
+
if (args.priority !== undefined)
|
|
312
|
+
body.priority = args.priority;
|
|
313
|
+
if (args.triage_enabled !== undefined)
|
|
314
|
+
body.triage_enabled = args.triage_enabled;
|
|
315
|
+
const result = await postWorkerRoute(url, body, "slack_recovery_create_or_link", dashboardBearer());
|
|
316
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
317
|
+
});
|
|
318
|
+
// ---------------- quality_gate_complete (absorbs quality_gate_skip, DX-3394 problem 1682) ----------------
|
|
319
|
+
const qualityGateComplete = strictTool("quality_gate_complete", "Sign off a registered quality gate. Two flavors by the gate's PHASE: (PLAN, e.g. plan-architecture) — " +
|
|
320
|
+
"runs as its own dispatch; this call is that dispatch's TERMINAL call: status='pass' releases the " +
|
|
321
|
+
"dispatch (the picker launches the next gate or the work agent); status='fail' BLOCKS the card (your " +
|
|
322
|
+
"message becomes the reason) and stops the dispatch (do NOT follow up with danxbot_complete); " +
|
|
323
|
+
"status='retry' — the fixer loop: you edited the card into a sounder plan and want a fresh pass. " +
|
|
324
|
+
"(CODE, e.g. code-quality) — runs INLINE in your WORK session: stamps the gate + stores findings and " +
|
|
325
|
+
"does NOTHING else. You CONTINUE: fix any blocking findings, re-run to a pass, then finalize and " +
|
|
326
|
+
"complete the card normally. status='skipped' — the gate is OBVIOUSLY UNNECESSARY for this diff " +
|
|
327
|
+
"(a single-line / docs-only change, or nothing in this gate's lane): use `message` as the mandatory " +
|
|
328
|
+
`justification (≥${QUALITY_GATE_SKIP_REASON_MIN_CHARS} chars, the permanent accountability record a ` +
|
|
329
|
+
"reviewer will read) — never skip to dodge a real review. A skipped required gate does not block " +
|
|
330
|
+
"transitioning the card to complete (through danxbot_api) and does not re-dispatch a PRE gate.", {
|
|
331
|
+
gate: z.string().describe("Registered gate name, e.g. 'plan-architecture' or 'code-quality'."),
|
|
332
|
+
status: z.enum(["pass", "fail", "retry", "skipped"]),
|
|
333
|
+
message: z
|
|
334
|
+
.string()
|
|
335
|
+
.describe("Sign-off rationale / verdict summary. On a plan-gate fail this becomes the card's block reason. " +
|
|
336
|
+
`On 'skipped' this is the mandatory ≥${QUALITY_GATE_SKIP_REASON_MIN_CHARS}-char justification.`),
|
|
337
|
+
findings: z
|
|
338
|
+
.array(z
|
|
339
|
+
.object({
|
|
340
|
+
title: z.string(),
|
|
341
|
+
description: z.string().optional(),
|
|
342
|
+
status: z.string().optional(),
|
|
343
|
+
reasoning: z.string().optional(),
|
|
344
|
+
})
|
|
345
|
+
.strict())
|
|
346
|
+
.optional()
|
|
347
|
+
.describe("Code gates only — structured findings, one object per finding. Omit for plan gates."),
|
|
348
|
+
}, async (args) => {
|
|
349
|
+
const url = readLifecycleUrlsFromEnv().qualityGate;
|
|
350
|
+
if (!url)
|
|
351
|
+
throw new Error("quality_gate_complete called outside a quality-gate-capable dispatch");
|
|
352
|
+
if (args.status === "skipped" &&
|
|
353
|
+
args.message.trim().length < QUALITY_GATE_SKIP_REASON_MIN_CHARS) {
|
|
354
|
+
throw new Error(`quality_gate_complete: message must be at least ${QUALITY_GATE_SKIP_REASON_MIN_CHARS} characters ` +
|
|
355
|
+
`for status:'skipped' — it is the accountability record for skipping the "${args.gate}" gate`);
|
|
356
|
+
}
|
|
357
|
+
const body = { gate: args.gate, status: args.status, message: args.message };
|
|
358
|
+
if (args.findings !== undefined)
|
|
359
|
+
body.findings = args.findings;
|
|
360
|
+
const result = await postWorkerRoute(url, body, "quality_gate_complete", agentBearer());
|
|
361
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
362
|
+
});
|
|
363
|
+
// ---------------- danxbot_set_evaluator_summary ----------------
|
|
364
|
+
const evaluatorSummary = strictTool("danxbot_set_evaluator_summary", "Write the root-cause summary for a 3-strike broken agent. ONLY the system-evaluator dispatch sees this " +
|
|
365
|
+
"tool. Call exactly once at the END of the dispatch, immediately before danxbot_complete. A dispatch " +
|
|
366
|
+
"that exits without calling this tool is treated as evaluator failure.", {
|
|
367
|
+
reason: z
|
|
368
|
+
.string()
|
|
369
|
+
.describe("Markdown body the dashboard banner renders: ## Root cause(s) (1-3 bullets), ## Per-strike detail " +
|
|
370
|
+
"(one bullet per strike), ## Recommended human action (one paragraph)."),
|
|
371
|
+
suggested_steps: z
|
|
372
|
+
.array(z.string())
|
|
373
|
+
.optional()
|
|
374
|
+
.describe("Ordered list of concrete operator actions. Empty/absent is allowed."),
|
|
375
|
+
}, async (args) => {
|
|
376
|
+
const url = readLifecycleUrlsFromEnv().evaluatorSummary;
|
|
377
|
+
if (!url)
|
|
378
|
+
throw new Error("danxbot_set_evaluator_summary called without an evaluator dispatch");
|
|
379
|
+
const result = await postWorkerRoute(url, { reason: args.reason, suggested_steps: args.suggested_steps ?? [] }, "danxbot_set_evaluator_summary", agentBearer());
|
|
380
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
381
|
+
});
|
|
382
|
+
// ---------------- save_template_app ----------------
|
|
383
|
+
const saveTemplateApp = strictTool("save_template_app", "Build and ship ONE template app, keyed by template id. Walks <cwd>/templates/<template_id>/source/, " +
|
|
384
|
+
"builds, packs the bundle, then on success fires an async callback to the repo's configured save_url. " +
|
|
385
|
+
"Returns the synchronous build verdict. Call once per app after its edits are complete.", {
|
|
386
|
+
template_id: z
|
|
387
|
+
.string()
|
|
388
|
+
.describe("Receiver-side template id — selects the source location and keys the output bundle."),
|
|
389
|
+
}, async (args) => {
|
|
390
|
+
const url = readLifecycleUrlsFromEnv().saveTemplateApp;
|
|
391
|
+
if (!url)
|
|
392
|
+
throw new Error("save_template_app called without a template_app-configured dispatch");
|
|
393
|
+
const result = await postWorkerRoute(url, { template_id: args.template_id }, "save_template_app", agentBearer());
|
|
394
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
395
|
+
});
|
|
396
|
+
// ---------------- load_template_app ----------------
|
|
397
|
+
const loadTemplateApp = strictTool("load_template_app", "Set up ONE template app on demand and bring its live HMR preview up. On a FRESH dispatch this WHOLESALE-" +
|
|
398
|
+
"RESETS <cwd>/templates/<template_id>/source/ (pulling the consumer's source or scaffolding a default) " +
|
|
399
|
+
"— call this BEFORE you start editing, never after. On a RESUMED dispatch it touches nothing. Returns " +
|
|
400
|
+
"{template_id, hmr_url, source_dir} — source_dir is the ONLY path to edit; the build reads it directly.", {
|
|
401
|
+
template_id: z
|
|
402
|
+
.string()
|
|
403
|
+
.describe("Receiver-side template id — defines the source location and is echoed to consumer callbacks."),
|
|
404
|
+
}, async (args) => {
|
|
405
|
+
const url = readLifecycleUrlsFromEnv().loadTemplateApp;
|
|
406
|
+
if (!url)
|
|
407
|
+
throw new Error("load_template_app called without a template_app-configured dispatch");
|
|
408
|
+
const result = await postWorkerRoute(url, { template_id: args.template_id }, "load_template_app", agentBearer());
|
|
409
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
410
|
+
});
|
|
411
|
+
return {
|
|
412
|
+
slackReply,
|
|
413
|
+
slackPostUpdate,
|
|
414
|
+
linkThreadToIssue,
|
|
415
|
+
slackRecoveryList,
|
|
416
|
+
slackRecoveryMessage,
|
|
417
|
+
slackRecoveryCreateOrLink,
|
|
418
|
+
qualityGateComplete,
|
|
419
|
+
evaluatorSummary,
|
|
420
|
+
saveTemplateApp,
|
|
421
|
+
loadTemplateApp,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Disable every conditionally-visible lifecycle tool whose URL is absent
|
|
426
|
+
* from `urls` — the same advertise-filter contract `buildActiveTools` used
|
|
427
|
+
* to enforce in the old standalone server. `danxbot_complete` is never in
|
|
428
|
+
* this map — it is unconditionally visible (every dispatch ends with it).
|
|
429
|
+
*/
|
|
430
|
+
export function applyLifecycleVisibility(tools, urls) {
|
|
431
|
+
const setVisible = (tool, visible) => {
|
|
432
|
+
if (visible)
|
|
433
|
+
tool.enable();
|
|
434
|
+
else
|
|
435
|
+
tool.disable();
|
|
436
|
+
};
|
|
437
|
+
setVisible(tools.slackReply, !!urls.slackReply);
|
|
438
|
+
setVisible(tools.slackPostUpdate, !!urls.slackUpdate);
|
|
439
|
+
setVisible(tools.linkThreadToIssue, !!urls.linkThreadToIssue);
|
|
440
|
+
setVisible(tools.slackRecoveryList, !!urls.slackRecoveryList);
|
|
441
|
+
setVisible(tools.slackRecoveryMessage, !!urls.slackRecoveryMessage);
|
|
442
|
+
setVisible(tools.slackRecoveryCreateOrLink, !!urls.slackRecoveryCreateOrLink);
|
|
443
|
+
setVisible(tools.qualityGateComplete, !!urls.qualityGate);
|
|
444
|
+
setVisible(tools.evaluatorSummary, !!urls.evaluatorSummary);
|
|
445
|
+
setVisible(tools.saveTemplateApp, !!urls.saveTemplateApp);
|
|
446
|
+
setVisible(tools.loadTemplateApp, !!urls.loadTemplateApp);
|
|
447
|
+
}
|
package/dist/priority.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tier-word → numeric priority resolution for
|
|
3
|
-
* (DX-1532).
|
|
2
|
+
* Tier-word → numeric priority resolution for `danxbot_api`'s
|
|
3
|
+
* `PATCH /api/issues/:id/edit` route (DX-1532).
|
|
4
4
|
*
|
|
5
5
|
* Priority is a STRUCTURED numeric column `issues.priority` (float in `[0,6)`).
|
|
6
6
|
* Both the Trello "priority" label and the dashboard priority badge read it via
|
|
7
7
|
* `priorityTier(issue.priority)` — description PROSE ("Priority: Low") is read
|
|
8
8
|
* by neither. So an agent that "sets priority" by editing description text
|
|
9
9
|
* produces a silent false-positive: nothing downstream changes. The fix is to
|
|
10
|
-
* let the agent write the numeric column through
|
|
10
|
+
* let the agent write the numeric column through the issue-edit route.
|
|
11
11
|
*
|
|
12
12
|
* Agents (and operators) speak in tier WORDS ("low" / "high"), so the tool
|
|
13
13
|
* accepts a tier word and resolves it to the tier's MIDPOINT here — the inverse
|
|
@@ -64,7 +64,7 @@ export function resolvePriority(input) {
|
|
|
64
64
|
return input;
|
|
65
65
|
const midpoint = PRIORITY_TIER_MIDPOINTS[input];
|
|
66
66
|
if (midpoint === undefined) {
|
|
67
|
-
throw new Error(`
|
|
67
|
+
throw new Error(`issue-edit: unknown priority tier word "${input}" (expected one of ${PRIORITY_TIER_WORDS.join(", ")} or a number in [0,6))`);
|
|
68
68
|
}
|
|
69
69
|
return midpoint;
|
|
70
70
|
}
|
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.164",
|
|
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",
|