@rubytech/taskmaster 1.44.5 → 1.44.7

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.
@@ -166,7 +166,10 @@ export function handleToolExecutionEnd(ctx, evt) {
166
166
  isError: isToolError,
167
167
  },
168
168
  });
169
- ctx.log.debug(`embedded run tool end: runId=${ctx.params.runId} tool=${toolName} toolCallId=${toolCallId}`);
169
+ const errorSuffix = isToolError
170
+ ? ` error=${extractToolErrorMessage(sanitizedResult) ?? "unknown"}`
171
+ : "";
172
+ ctx.log.debug(`embedded run tool end: runId=${ctx.params.runId} tool=${toolName} toolCallId=${toolCallId} isError=${isToolError}${errorSuffix}`);
170
173
  if (ctx.params.onToolResult && ctx.shouldEmitToolOutput()) {
171
174
  const outputText = extractToolResultText(sanitizedResult);
172
175
  if (outputText) {
@@ -11,7 +11,7 @@ import { assertRequiredParams, CLAUDE_PARAM_GROUPS, createTaskmasterReadTool, cr
11
11
  import { cleanToolSchemaForGemini, normalizeToolParameters } from "./pi-tools.schema.js";
12
12
  import { buildPluginToolGroups, collectExplicitAllowlist, expandPolicyWithPluginGroups, normalizeToolName, resolveToolProfilePolicy, stripPluginOnlyAllowlist, } from "./tool-policy.js";
13
13
  import { getPluginToolMeta } from "../plugins/tools.js";
14
- import { logWarn } from "../logger.js";
14
+ import { logInfo, logWarn } from "../logger.js";
15
15
  function isOpenAIProvider(provider) {
16
16
  const normalized = provider?.trim().toLowerCase();
17
17
  return normalized === "openai" || normalized === "openai-codex";
@@ -254,33 +254,49 @@ export function createTaskmasterCodingTools(options) {
254
254
  const groupPolicyExpanded = resolvePolicy(groupPolicy, "group tools.allow");
255
255
  const sandboxPolicyExpanded = expandPolicyWithPluginGroups(sandbox?.tools, pluginGroups);
256
256
  const subagentPolicyExpanded = expandPolicyWithPluginGroups(subagentPolicy, pluginGroups);
257
- const toolsFiltered = profilePolicyExpanded
258
- ? filterToolsByPolicy(tools, profilePolicyExpanded)
259
- : tools;
260
- const providerProfileFiltered = providerProfileExpanded
261
- ? filterToolsByPolicy(toolsFiltered, providerProfileExpanded)
262
- : toolsFiltered;
263
- const globalFiltered = globalPolicyExpanded
264
- ? filterToolsByPolicy(providerProfileFiltered, globalPolicyExpanded)
265
- : providerProfileFiltered;
266
- const globalProviderFiltered = globalProviderExpanded
267
- ? filterToolsByPolicy(globalFiltered, globalProviderExpanded)
268
- : globalFiltered;
269
- const agentFiltered = agentPolicyExpanded
270
- ? filterToolsByPolicy(globalProviderFiltered, agentPolicyExpanded)
271
- : globalProviderFiltered;
272
- const agentProviderFiltered = agentProviderExpanded
273
- ? filterToolsByPolicy(agentFiltered, agentProviderExpanded)
274
- : agentFiltered;
275
- const groupFiltered = groupPolicyExpanded
276
- ? filterToolsByPolicy(agentProviderFiltered, groupPolicyExpanded)
277
- : agentProviderFiltered;
278
- const sandboxed = sandboxPolicyExpanded
279
- ? filterToolsByPolicy(groupFiltered, sandboxPolicyExpanded)
280
- : groupFiltered;
281
- const subagentFiltered = subagentPolicyExpanded
282
- ? filterToolsByPolicy(sandboxed, subagentPolicyExpanded)
283
- : sandboxed;
257
+ // ── Cascading tool filter with diagnostic logging ─────────────────────
258
+ const filterStage = (input, policy, label) => {
259
+ if (!policy)
260
+ return input;
261
+ const output = filterToolsByPolicy(input, policy);
262
+ const inputNames = new Set(input.map((t) => normalizeToolName(t.name)));
263
+ const outputNames = new Set(output.map((t) => normalizeToolName(t.name)));
264
+ const removed = [...inputNames].filter((n) => !outputNames.has(n));
265
+ if (removed.length > 0) {
266
+ const sessionLabel = options?.sessionKey ?? "unknown-session";
267
+ logInfo(`[tool-filter] ${sessionLabel} | ${label} removed ${removed.length} tools: ${removed.join(", ")}`);
268
+ }
269
+ return output;
270
+ };
271
+ // When the agent has an explicit allow list, merge it with the profile's
272
+ // allow list so the agent can grant tools beyond the profile baseline.
273
+ // The profile sets a default scope; the agent config can expand it.
274
+ // Deny lists and PROFILE_NEVER_GRANT still restrict regardless.
275
+ const effectiveProfilePolicy = (() => {
276
+ if (!profilePolicyExpanded || !agentPolicyExpanded?.allow?.length) {
277
+ return profilePolicyExpanded;
278
+ }
279
+ const mergedAllow = [
280
+ ...(profilePolicyExpanded.allow ?? []),
281
+ ...(agentPolicyExpanded.allow ?? []),
282
+ ];
283
+ return { ...profilePolicyExpanded, allow: mergedAllow };
284
+ })();
285
+ const toolsFiltered = filterStage(tools, effectiveProfilePolicy, `profile(${effectiveProfile ?? "none"})+agent`);
286
+ const providerProfileFiltered = filterStage(toolsFiltered, providerProfileExpanded, "providerProfile");
287
+ const globalFiltered = filterStage(providerProfileFiltered, globalPolicyExpanded, "global");
288
+ const globalProviderFiltered = filterStage(globalFiltered, globalProviderExpanded, "globalProvider");
289
+ const agentFiltered = filterStage(globalProviderFiltered, agentPolicyExpanded, `agent(${agentId ?? "?"})`);
290
+ const agentProviderFiltered = filterStage(agentFiltered, agentProviderExpanded, "agentProvider");
291
+ const groupFiltered = filterStage(agentProviderFiltered, groupPolicyExpanded, "group");
292
+ const sandboxed = filterStage(groupFiltered, sandboxPolicyExpanded, "sandbox");
293
+ const subagentFiltered = filterStage(sandboxed, subagentPolicyExpanded, "subagent");
294
+ // Log final tool set for observability.
295
+ {
296
+ const sessionLabel = options?.sessionKey ?? "unknown-session";
297
+ const finalNames = subagentFiltered.map((t) => normalizeToolName(t.name)).sort();
298
+ logInfo(`[tool-filter] ${sessionLabel} | profile=${effectiveProfile ?? "none"} | final tools (${finalNames.length}): ${finalNames.join(", ")}`);
299
+ }
284
300
  // Always normalize tool JSON Schemas before handing them to pi-agent/pi-ai.
285
301
  // Without this, some providers (notably OpenAI) will reject root-level union schemas.
286
302
  const normalized = subagentFiltered.map(normalizeToolParameters);
@@ -338,12 +338,15 @@ export function buildAgentSystemPrompt(params) {
338
338
  "",
339
339
  "### Spawn Profiles",
340
340
  "Use `toolProfile` to give sub-agents only the tools they need. This reduces cost and focuses the sub-agent.",
341
+ "",
342
+ "**Profile selection principle:** match the profile to the task's **output**, not its input or subject matter. A task that reads from memory but produces a PDF needs `spawn:documents`, not `spawn:memory`. A task that reads a document but saves findings to memory needs `spawn:memory`, not `spawn:documents`. The deliverable determines the profile.",
343
+ "",
341
344
  "Available profiles:",
342
- "- `spawn:memory` — recall and store information: memory search, read, write, file access, skills",
343
- "- `spawn:documents` — read and produce documents: PDFs, spreadsheets, QR codes",
345
+ "- `spawn:memory` — recall, search, and store information in memory. For lookups, saving notes, and organizing knowledge — NOT for producing formatted output files like PDFs.",
346
+ "- `spawn:documents` — produce formatted output files: draft content, convert to PDF, generate QR codes. Has memory access so it can read source material. Use whenever the deliverable is a file (PDF, document, spreadsheet).",
344
347
  "- `spawn:contacts` — manage contacts: lookup, create, update, verify",
345
348
  "- `spawn:web` — research online: web search, URL fetch, file access",
346
- "- `spawn:admin` — system administration and diagnostics: manage admins, status checks, settings, log review, error triage, performance analysis, automation",
349
+ "- `spawn:admin` — system administration, licensing, and diagnostics: manage admins, generate licenses, status checks, settings, log review, error triage, performance analysis, automation",
347
350
  "- `spawn:automation` — scheduled tasks: cron jobs, gateway management, updates, tunnel",
348
351
  "- `spawn:ui` — visual work: browser control, canvas",
349
352
  "- `spawn:worker` — general-purpose (files, memory, web, docs, images, skills)",
@@ -68,6 +68,7 @@ export const TOOL_GROUPS = {
68
68
  "account_manage",
69
69
  "access_manage",
70
70
  "license_manage",
71
+ "license_generate",
71
72
  ],
72
73
  // All Taskmaster native tools (excludes provider plugins).
73
74
  "group:taskmaster": [
@@ -123,6 +124,7 @@ export const TOOL_GROUPS = {
123
124
  "account_manage",
124
125
  "access_manage",
125
126
  "license_manage",
127
+ "license_generate",
126
128
  "tunnel_status",
127
129
  "tunnel_enable",
128
130
  "tunnel_disable",
@@ -205,7 +207,15 @@ const TOOL_PROFILES = {
205
207
  allow: ["group:memory", "group:fs", "group:runtime", "group:time", "group:skills", "image"],
206
208
  },
207
209
  "spawn:documents": {
208
- allow: ["group:documents", "group:fs", "group:runtime", "group:time", "group:skills", "image"],
210
+ allow: [
211
+ "group:documents",
212
+ "group:memory",
213
+ "group:fs",
214
+ "group:runtime",
215
+ "group:time",
216
+ "group:skills",
217
+ "image",
218
+ ],
209
219
  },
210
220
  "spawn:contacts": {
211
221
  allow: ["group:contacts", "group:fs", "group:time", "group:skills"],
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs/promises";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
4
  import { pathToFileURL } from "node:url";
4
5
  import { Type } from "@sinclair/typebox";
@@ -7,12 +8,48 @@ import { resolveBrowserExecutableForPlatform } from "../../browser/chrome.execut
7
8
  import { resolveAgentWorkspaceDir, resolveSessionAgentId } from "../agent-scope.js";
8
9
  import { assertSandboxPath } from "../sandbox-paths.js";
9
10
  import { jsonResult, readStringParam } from "./common.js";
11
+ const MARKDOWN_EXTENSIONS = new Set([".md", ".markdown", ".mdown", ".mkd"]);
10
12
  const DocumentToPdfSchema = Type.Object({
11
13
  path: Type.String({
12
- description: "Path to an HTML file. Use a memory path (e.g. memory/users/+447.../documents/invoices/INV-001.html) " +
13
- "or an absolute path.",
14
+ description: "Path to an HTML or Markdown file to convert to PDF. " +
15
+ "Accepts .html, .md, and .markdown files. Markdown files are automatically rendered to styled HTML before conversion. " +
16
+ "Use a memory path (e.g. memory/admin/documents/contract.md) or an absolute path.",
14
17
  }),
15
18
  });
19
+ /**
20
+ * Wrap markdown-rendered HTML in a styled document shell suitable for PDF output.
21
+ * Uses system fonts and clean typographic defaults so the PDF looks professional
22
+ * without requiring external stylesheets.
23
+ */
24
+ function wrapMarkdownHtml(bodyHtml) {
25
+ return `<!doctype html>
26
+ <html><head>
27
+ <meta charset="utf-8" />
28
+ <style>
29
+ html { font-size: 14px; }
30
+ body {
31
+ max-width: 720px; margin: 40px auto; padding: 0 24px;
32
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
33
+ line-height: 1.6; color: #1a1a1a;
34
+ }
35
+ h1 { font-size: 1.8rem; margin: 1.5em 0 0.5em; border-bottom: 1px solid #ddd; padding-bottom: 0.3em; }
36
+ h2 { font-size: 1.4rem; margin: 1.3em 0 0.4em; }
37
+ h3 { font-size: 1.15rem; margin: 1.2em 0 0.4em; }
38
+ p { margin: 0.6em 0; }
39
+ ul, ol { padding-left: 1.8em; }
40
+ li { margin: 0.25em 0; }
41
+ table { border-collapse: collapse; width: 100%; margin: 1em 0; }
42
+ th, td { border: 1px solid #ccc; padding: 6px 10px; text-align: left; }
43
+ th { background: #f5f5f5; font-weight: 600; }
44
+ blockquote { border-left: 3px solid #ccc; margin: 1em 0; padding: 0.5em 1em; color: #555; }
45
+ code { background: #f4f4f4; padding: 2px 5px; border-radius: 3px; font-size: 0.9em; }
46
+ pre { background: #f4f4f4; padding: 12px; border-radius: 6px; overflow-x: auto; }
47
+ pre code { background: none; padding: 0; }
48
+ hr { border: none; border-top: 1px solid #ddd; margin: 2em 0; }
49
+ strong { font-weight: 600; }
50
+ </style>
51
+ </head><body>${bodyHtml}</body></html>`;
52
+ }
16
53
  export function createDocumentToPdfTool(options) {
17
54
  const cfg = options?.config;
18
55
  const agentId = resolveSessionAgentId({
@@ -25,54 +62,55 @@ export function createDocumentToPdfTool(options) {
25
62
  return {
26
63
  label: "Document to PDF",
27
64
  name: "document_to_pdf",
28
- description: "Convert an HTML file to PDF using a headless browser. " +
65
+ description: "Convert an HTML or Markdown file to PDF using a headless browser. " +
66
+ "Markdown files (.md) are automatically rendered to styled HTML before conversion. " +
29
67
  "The PDF is written to the same directory as the source file with a .pdf extension. " +
30
68
  "Returns the absolute path to the generated PDF.",
31
69
  parameters: DocumentToPdfSchema,
32
70
  execute: async (_toolCallId, params) => {
33
71
  const inputPath = readStringParam(params, "path", { required: true });
34
72
  // ── Resolve the path ──────────────────────────────────────────────
35
- let htmlPath;
73
+ let sourcePath;
36
74
  try {
37
75
  if (memoryDir && inputPath.startsWith("memory/")) {
38
76
  const relativePath = inputPath.slice("memory/".length);
39
- htmlPath = path.join(memoryDir, relativePath);
77
+ sourcePath = path.join(memoryDir, relativePath);
40
78
  }
41
79
  else if (memoryDir && !path.isAbsolute(inputPath) && !inputPath.startsWith(".")) {
42
80
  // Check if it exists in memory root
43
81
  const candidatePath = path.join(memoryDir, inputPath);
44
82
  try {
45
83
  await fs.stat(candidatePath);
46
- htmlPath = candidatePath;
84
+ sourcePath = candidatePath;
47
85
  }
48
86
  catch {
49
- htmlPath = inputPath;
87
+ sourcePath = inputPath;
50
88
  }
51
89
  }
52
90
  else {
53
- htmlPath = inputPath;
91
+ sourcePath = inputPath;
54
92
  }
55
- if (!path.isAbsolute(htmlPath)) {
56
- htmlPath = path.resolve(htmlPath);
93
+ if (!path.isAbsolute(sourcePath)) {
94
+ sourcePath = path.resolve(sourcePath);
57
95
  }
58
96
  // Apply sandbox constraints if configured
59
97
  const sandboxRoot = options?.sandboxRoot?.trim();
60
98
  if (sandboxRoot) {
61
99
  const sandboxed = await assertSandboxPath({
62
- filePath: htmlPath,
100
+ filePath: sourcePath,
63
101
  cwd: sandboxRoot,
64
102
  root: sandboxRoot,
65
103
  });
66
- htmlPath = sandboxed.resolved;
104
+ sourcePath = sandboxed.resolved;
67
105
  }
68
106
  }
69
107
  catch (err) {
70
108
  const message = err instanceof Error ? err.message : String(err);
71
109
  return jsonResult({ ok: false, error: `Path not accessible: ${message}` });
72
110
  }
73
- // ── Verify the HTML file exists ───────────────────────────────────
111
+ // ── Verify the source file exists ─────────────────────────────────
74
112
  try {
75
- await fs.stat(htmlPath);
113
+ await fs.stat(sourcePath);
76
114
  }
77
115
  catch (err) {
78
116
  const anyErr = err;
@@ -83,8 +121,31 @@ export function createDocumentToPdfTool(options) {
83
121
  return jsonResult({ ok: false, error: `Cannot access file: ${message}` });
84
122
  }
85
123
  // ── Determine output path ─────────────────────────────────────────
86
- const parsed = path.parse(htmlPath);
124
+ const parsed = path.parse(sourcePath);
87
125
  const pdfPath = path.join(parsed.dir, `${parsed.name}.pdf`);
126
+ const isMarkdown = MARKDOWN_EXTENSIONS.has(parsed.ext.toLowerCase());
127
+ // ── If markdown, render to a temp HTML file ───────────────────────
128
+ let htmlPath;
129
+ let tempHtmlPath;
130
+ if (isMarkdown) {
131
+ try {
132
+ const mdSource = await fs.readFile(sourcePath, "utf-8");
133
+ const MarkdownIt = (await import("markdown-it")).default;
134
+ const md = new MarkdownIt({ html: true, linkify: true, typographer: true });
135
+ const bodyHtml = md.render(mdSource);
136
+ const fullHtml = wrapMarkdownHtml(bodyHtml);
137
+ tempHtmlPath = path.join(os.tmpdir(), `taskmaster-pdf-${Date.now()}-${parsed.name}.html`);
138
+ await fs.writeFile(tempHtmlPath, fullHtml, "utf-8");
139
+ htmlPath = tempHtmlPath;
140
+ }
141
+ catch (err) {
142
+ const message = err instanceof Error ? err.message : String(err);
143
+ return jsonResult({ ok: false, error: `Markdown conversion failed: ${message}` });
144
+ }
145
+ }
146
+ else {
147
+ htmlPath = sourcePath;
148
+ }
88
149
  // ── Find Chrome executable ────────────────────────────────────────
89
150
  const browserConfig = resolveBrowserConfig(cfg?.browser);
90
151
  const exe = resolveBrowserExecutableForPlatform(browserConfig, process.platform);
@@ -130,6 +191,10 @@ export function createDocumentToPdfTool(options) {
130
191
  // Ignore close errors
131
192
  }
132
193
  }
194
+ // Clean up temp HTML file if we created one
195
+ if (tempHtmlPath) {
196
+ fs.unlink(tempHtmlPath).catch(() => { });
197
+ }
133
198
  }
134
199
  },
135
200
  };
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.44.5",
3
- "commit": "414cf5a6f0475e90da9e44c9a38af23eb2662ffa",
4
- "builtAt": "2026-03-25T13:57:46.756Z"
2
+ "version": "1.44.7",
3
+ "commit": "842965d43dfa6cdab9cb31e489c9a295cf755dfb",
4
+ "builtAt": "2026-03-28T11:06:01.275Z"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/taskmaster",
3
- "version": "1.44.5",
3
+ "version": "1.44.7",
4
4
  "description": "AI-powered business assistant for small businesses",
5
5
  "publishConfig": {
6
6
  "access": "public"