@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.
- package/dist/agents/pi-embedded-subscribe.handlers.tools.js +4 -1
- package/dist/agents/pi-tools.js +44 -28
- package/dist/agents/system-prompt.js +6 -3
- package/dist/agents/tool-policy.js +11 -1
- package/dist/agents/tools/document-to-pdf-tool.js +80 -15
- package/dist/build-info.json +3 -3
- package/package.json +1 -1
|
@@ -166,7 +166,10 @@ export function handleToolExecutionEnd(ctx, evt) {
|
|
|
166
166
|
isError: isToolError,
|
|
167
167
|
},
|
|
168
168
|
});
|
|
169
|
-
|
|
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) {
|
package/dist/agents/pi-tools.js
CHANGED
|
@@ -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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
:
|
|
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
|
|
343
|
-
"- `spawn:documents` —
|
|
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: [
|
|
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
|
|
13
|
-
"
|
|
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
|
|
73
|
+
let sourcePath;
|
|
36
74
|
try {
|
|
37
75
|
if (memoryDir && inputPath.startsWith("memory/")) {
|
|
38
76
|
const relativePath = inputPath.slice("memory/".length);
|
|
39
|
-
|
|
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
|
-
|
|
84
|
+
sourcePath = candidatePath;
|
|
47
85
|
}
|
|
48
86
|
catch {
|
|
49
|
-
|
|
87
|
+
sourcePath = inputPath;
|
|
50
88
|
}
|
|
51
89
|
}
|
|
52
90
|
else {
|
|
53
|
-
|
|
91
|
+
sourcePath = inputPath;
|
|
54
92
|
}
|
|
55
|
-
if (!path.isAbsolute(
|
|
56
|
-
|
|
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:
|
|
100
|
+
filePath: sourcePath,
|
|
63
101
|
cwd: sandboxRoot,
|
|
64
102
|
root: sandboxRoot,
|
|
65
103
|
});
|
|
66
|
-
|
|
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
|
|
111
|
+
// ── Verify the source file exists ─────────────────────────────────
|
|
74
112
|
try {
|
|
75
|
-
await fs.stat(
|
|
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(
|
|
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
|
};
|
package/dist/build-info.json
CHANGED