@rubytech/taskmaster 1.0.10 → 1.0.11

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.
@@ -138,6 +138,10 @@ export async function compactEmbeddedPiSessionDirect(params) {
138
138
  sessionId: params.sessionId,
139
139
  warn: makeBootstrapWarn({ sessionLabel, warn: (message) => log.warn(message) }),
140
140
  });
141
+ // Extract skill base dirs for scoped skill_read tool
142
+ const skillBaseDirs = (params.skillsSnapshot?.resolvedSkills ?? [])
143
+ .map((s) => s.baseDir)
144
+ .filter(Boolean);
141
145
  const runAbortController = new AbortController();
142
146
  const toolsRaw = createTaskmasterCodingTools({
143
147
  exec: {
@@ -159,6 +163,7 @@ export async function compactEmbeddedPiSessionDirect(params) {
159
163
  modelProvider: model.provider,
160
164
  modelId,
161
165
  modelAuthMode: resolveModelAuthMode(model.provider, params.config),
166
+ skillBaseDirs,
162
167
  });
163
168
  const tools = sanitizeToolsForGoogle({ tools: toolsRaw, provider });
164
169
  logToolSchemasForGoogle({ tools, provider });
@@ -254,6 +259,7 @@ export async function compactEmbeddedPiSessionDirect(params) {
254
259
  extraSystemPrompt: params.extraSystemPrompt,
255
260
  ownerNumbers: params.ownerNumbers,
256
261
  reasoningTagHint,
262
+ finalTagHint: !reasoningTagHint,
257
263
  heartbeatPrompt: isDefaultAgent
258
264
  ? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
259
265
  : undefined,
@@ -143,6 +143,10 @@ export async function runEmbeddedAttempt(params) {
143
143
  ? ["Reminder: commit your changes in this workspace after edits."]
144
144
  : undefined;
145
145
  const agentDir = params.agentDir ?? resolveTaskmasterAgentDir();
146
+ // Extract skill base dirs for scoped skill_read tool
147
+ const skillBaseDirs = (params.skillsSnapshot?.resolvedSkills ?? [])
148
+ .map((s) => s.baseDir)
149
+ .filter(Boolean);
146
150
  // Check if the model supports native image input
147
151
  const modelHasVision = params.model.input?.includes("image") ?? false;
148
152
  const toolsRaw = params.disableTools
@@ -175,6 +179,7 @@ export async function runEmbeddedAttempt(params) {
175
179
  hasRepliedRef: params.hasRepliedRef,
176
180
  modelHasVision,
177
181
  skipMessageDelivery: params.skipMessageDelivery,
182
+ skillBaseDirs,
178
183
  });
179
184
  const tools = sanitizeToolsForGoogle({ tools: toolsRaw, provider: params.provider });
180
185
  logToolSchemasForGoogle({ tools, provider: params.provider });
@@ -279,6 +284,7 @@ export async function runEmbeddedAttempt(params) {
279
284
  extraSystemPrompt: params.extraSystemPrompt,
280
285
  ownerNumbers: params.ownerNumbers,
281
286
  reasoningTagHint,
287
+ finalTagHint: !reasoningTagHint,
282
288
  heartbeatPrompt: isDefaultAgent
283
289
  ? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
284
290
  : undefined,
@@ -8,6 +8,7 @@ export function buildEmbeddedSystemPrompt(params) {
8
8
  extraSystemPrompt: params.extraSystemPrompt,
9
9
  ownerNumbers: params.ownerNumbers,
10
10
  reasoningTagHint: params.reasoningTagHint,
11
+ finalTagHint: params.finalTagHint,
11
12
  heartbeatPrompt: params.heartbeatPrompt,
12
13
  skillsPrompt: params.skillsPrompt,
13
14
  docsPath: params.docsPath,
@@ -218,6 +218,7 @@ export function createTaskmasterCodingTools(options) {
218
218
  modelHasVision: options?.modelHasVision,
219
219
  requesterAgentIdOverride: agentId,
220
220
  messageSkipDelivery: options?.skipMessageDelivery,
221
+ skillBaseDirs: options?.skillBaseDirs,
221
222
  }),
222
223
  ];
223
224
  const coreToolNames = new Set(tools
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { formatSkillsForPrompt, loadSkillsFromDir, } from "@mariozechner/pi-coding-agent";
3
+ import { loadSkillsFromDir } from "@mariozechner/pi-coding-agent";
4
4
  import { createSubsystemLogger } from "../../logging/subsystem.js";
5
5
  import { CONFIG_DIR, resolveUserPath } from "../../utils.js";
6
6
  import { resolveBundledSkillsDir } from "./bundled-dir.js";
@@ -10,6 +10,41 @@ import { resolvePluginSkillDirs } from "./plugin-skills.js";
10
10
  import { serializeByKey } from "./serialize.js";
11
11
  const fsp = fs.promises;
12
12
  const skillsLogger = createSubsystemLogger("skills");
13
+ function escapeXml(str) {
14
+ return str
15
+ .replace(/&/g, "&")
16
+ .replace(/</g, "&lt;")
17
+ .replace(/>/g, "&gt;")
18
+ .replace(/"/g, "&quot;")
19
+ .replace(/'/g, "&apos;");
20
+ }
21
+ /**
22
+ * Format skills for inclusion in a system prompt.
23
+ * Identical to the library's formatSkillsForPrompt except it references
24
+ * `skill_read` instead of `read` so agents denied group:fs can still
25
+ * load skill content.
26
+ */
27
+ function formatSkillsForPromptTaskmaster(skills) {
28
+ const visibleSkills = skills.filter((s) => !s.disableModelInvocation);
29
+ if (visibleSkills.length === 0)
30
+ return "";
31
+ const lines = [
32
+ "\n\nThe following skills provide specialized instructions for specific tasks.",
33
+ "Use the skill_read tool to load a skill's file when the task matches its description.",
34
+ "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
35
+ "",
36
+ "<available_skills>",
37
+ ];
38
+ for (const skill of visibleSkills) {
39
+ lines.push(" <skill>");
40
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
41
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
42
+ lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
43
+ lines.push(" </skill>");
44
+ }
45
+ lines.push("</available_skills>");
46
+ return lines.join("\n");
47
+ }
13
48
  const skillCommandDebugOnce = new Set();
14
49
  function debugSkillCommandOnce(messageKey, message, meta) {
15
50
  if (skillCommandDebugOnce.has(messageKey))
@@ -141,7 +176,9 @@ export function buildWorkspaceSkillSnapshot(workspaceDir, opts) {
141
176
  const promptEntries = eligible.filter((entry) => entry.invocation?.disableModelInvocation !== true);
142
177
  const resolvedSkills = promptEntries.map((entry) => entry.skill);
143
178
  const remoteNote = opts?.eligibility?.remote?.note?.trim();
144
- const prompt = [remoteNote, formatSkillsForPrompt(resolvedSkills)].filter(Boolean).join("\n");
179
+ const prompt = [remoteNote, formatSkillsForPromptTaskmaster(resolvedSkills)]
180
+ .filter(Boolean)
181
+ .join("\n");
145
182
  return {
146
183
  prompt,
147
184
  skills: eligible.map((entry) => ({
@@ -157,7 +194,7 @@ export function buildWorkspaceSkillsPrompt(workspaceDir, opts) {
157
194
  const eligible = filterSkillEntries(skillEntries, opts?.config, opts?.skillFilter, opts?.eligibility);
158
195
  const promptEntries = eligible.filter((entry) => entry.invocation?.disableModelInvocation !== true);
159
196
  const remoteNote = opts?.eligibility?.remote?.note?.trim();
160
- return [remoteNote, formatSkillsForPrompt(promptEntries.map((entry) => entry.skill))]
197
+ return [remoteNote, formatSkillsForPromptTaskmaster(promptEntries.map((entry) => entry.skill))]
161
198
  .filter(Boolean)
162
199
  .join("\n");
163
200
  }
@@ -132,9 +132,11 @@ export function buildAgentSystemPrompt(params) {
132
132
  sessions_spawn: "Spawn a sub-agent session",
133
133
  session_status: "Show a /status-equivalent status card (usage + time + Reasoning/Verbose/Elevated); use for model-use questions (📊 session_status); optional per-session model override",
134
134
  image: "Analyze an image with the configured image model",
135
+ skill_read: "Read files within skill directories (SKILL.md, references)",
135
136
  };
136
137
  const toolOrder = [
137
138
  "read",
139
+ "skill_read",
138
140
  "write",
139
141
  "edit",
140
142
  "apply_patch",
@@ -192,6 +194,10 @@ export function buildAgentSystemPrompt(params) {
192
194
  }
193
195
  const hasGateway = availableTools.has("gateway");
194
196
  const readToolName = resolveToolName("read");
197
+ // For skills section: prefer skill_read when agent has it but not read
198
+ const skillReadToolName = availableTools.has("skill_read") && !availableTools.has("read")
199
+ ? resolveToolName("skill_read")
200
+ : readToolName;
195
201
  const execToolName = resolveToolName("exec");
196
202
  const processToolName = resolveToolName("process");
197
203
  const extraSystemPrompt = params.extraSystemPrompt?.trim();
@@ -211,6 +217,15 @@ export function buildAgentSystemPrompt(params) {
211
217
  "<final>Hey there! What would you like to do next?</final>",
212
218
  ].join(" ")
213
219
  : undefined;
220
+ // For providers with native thinking (e.g. Anthropic), we only need the <final> instruction
221
+ // to prevent internal reasoning/planning text from leaking to the user.
222
+ const finalOnlyHint = !reasoningHint && params.finalTagHint
223
+ ? [
224
+ "Wrap every user-facing reply in <final>...</final> tags.",
225
+ "Only text inside <final> is shown to the user; everything outside is discarded.",
226
+ "Do not narrate your plan or next steps outside <final> — the user cannot see it.",
227
+ ].join(" ")
228
+ : undefined;
214
229
  const reasoningLevel = params.reasoningLevel ?? "off";
215
230
  const userTimezone = params.userTimezone?.trim();
216
231
  const skillsPrompt = params.skillsPrompt?.trim();
@@ -231,7 +246,7 @@ export function buildAgentSystemPrompt(params) {
231
246
  const skillsSection = buildSkillsSection({
232
247
  skillsPrompt,
233
248
  isMinimal,
234
- readToolName,
249
+ readToolName: skillReadToolName,
235
250
  });
236
251
  const memorySection = buildMemorySection({ isMinimal, availableTools });
237
252
  const docsSection = buildDocsSection({
@@ -417,6 +432,9 @@ export function buildAgentSystemPrompt(params) {
417
432
  if (reasoningHint) {
418
433
  lines.push("## Reasoning Format", reasoningHint, "");
419
434
  }
435
+ else if (finalOnlyHint) {
436
+ lines.push("## Output Format", finalOnlyHint, "");
437
+ }
420
438
  const contextFiles = params.contextFiles ?? [];
421
439
  if (contextFiles.length > 0) {
422
440
  const hasSoulFile = contextFiles.some((file) => {
@@ -23,6 +23,7 @@ import { createLicenseGenerateTool } from "./tools/license-tool.js";
23
23
  import { createCustomerLookupTool } from "./tools/customer-lookup-tool.js";
24
24
  import { createCustomerUpdateTool } from "./tools/customer-update-tool.js";
25
25
  import { createRelayMessageTool } from "./tools/relay-message-tool.js";
26
+ import { createSkillReadTool } from "./tools/skill-read-tool.js";
26
27
  export function createTaskmasterTools(options) {
27
28
  const imageTool = options?.agentDir?.trim()
28
29
  ? createImageTool({
@@ -127,6 +128,11 @@ export function createTaskmasterTools(options) {
127
128
  createCustomerUpdateTool(),
128
129
  createRelayMessageTool(),
129
130
  ];
131
+ // Add scoped skill_read tool when skill directories are provided
132
+ const skillDirs = (options?.skillBaseDirs ?? []).filter(Boolean);
133
+ if (skillDirs.length > 0) {
134
+ tools.push(createSkillReadTool({ allowedSkillDirs: skillDirs }));
135
+ }
130
136
  // Add memory tools (conditionally based on config)
131
137
  const memoryToolOptions = {
132
138
  config: options?.config,
@@ -40,6 +40,11 @@
40
40
  "title": "Read",
41
41
  "detailKeys": ["path"]
42
42
  },
43
+ "skill_read": {
44
+ "emoji": "📖",
45
+ "title": "Skill Read",
46
+ "detailKeys": ["path"]
47
+ },
43
48
  "write": {
44
49
  "emoji": "✍️",
45
50
  "title": "Write",
@@ -0,0 +1,75 @@
1
+ import fs from "node:fs/promises";
2
+ import { realpath } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { Type } from "@sinclair/typebox";
5
+ import { readStringParam } from "./common.js";
6
+ const SkillReadSchema = Type.Object({
7
+ path: Type.String({ description: "Absolute or relative path to the file to read" }),
8
+ skill_dir: Type.Optional(Type.String({
9
+ description: "Skill directory for resolving relative paths. If omitted, path must be absolute.",
10
+ })),
11
+ });
12
+ /**
13
+ * Create a scoped file-read tool that only allows reading files within
14
+ * the provided skill directories. This gives agents access to skill
15
+ * content (SKILL.md, references/) without exposing the full filesystem.
16
+ */
17
+ export function createSkillReadTool(opts) {
18
+ // Pre-resolve allowed dirs at creation time (async realpath done at call time).
19
+ const allowedDirs = opts.allowedSkillDirs.filter(Boolean);
20
+ return {
21
+ label: "Skill Read",
22
+ name: "skill_read",
23
+ description: "Read a file within a skill directory (SKILL.md, references, etc.). " +
24
+ "Use this to load skill content on demand. " +
25
+ "Only files inside skill directories are accessible.",
26
+ parameters: SkillReadSchema,
27
+ execute: async (_toolCallId, args) => {
28
+ const params = args;
29
+ const rawPath = readStringParam(params, "path", { required: true });
30
+ const skillDir = readStringParam(params, "skill_dir");
31
+ // Resolve the target path
32
+ let targetPath;
33
+ if (path.isAbsolute(rawPath)) {
34
+ targetPath = path.resolve(rawPath);
35
+ }
36
+ else if (skillDir) {
37
+ targetPath = path.resolve(skillDir, rawPath);
38
+ }
39
+ else {
40
+ throw new Error("Relative path requires skill_dir. Provide an absolute path or include skill_dir.");
41
+ }
42
+ // Security: resolve symlinks on the target and check containment
43
+ let realTarget;
44
+ try {
45
+ realTarget = await realpath(targetPath);
46
+ }
47
+ catch {
48
+ throw new Error(`File not found: ${targetPath}`);
49
+ }
50
+ let allowed = false;
51
+ for (const dir of allowedDirs) {
52
+ let realDir;
53
+ try {
54
+ realDir = await realpath(dir);
55
+ }
56
+ catch {
57
+ continue;
58
+ }
59
+ const prefix = realDir.endsWith("/") ? realDir : `${realDir}/`;
60
+ if (realTarget === realDir || realTarget.startsWith(prefix)) {
61
+ allowed = true;
62
+ break;
63
+ }
64
+ }
65
+ if (!allowed) {
66
+ throw new Error(`Access denied: ${rawPath} is outside all skill directories.`);
67
+ }
68
+ const content = await fs.readFile(realTarget, "utf-8");
69
+ return {
70
+ content: [{ type: "text", text: content }],
71
+ details: { path: realTarget },
72
+ };
73
+ },
74
+ };
75
+ }
@@ -20,7 +20,7 @@ export const DEFAULT_IDENTITY_FILENAME = "IDENTITY.md";
20
20
  export const DEFAULT_USER_FILENAME = "USER.md";
21
21
  export const DEFAULT_HEARTBEAT_FILENAME = "HEARTBEAT.md";
22
22
  export const DEFAULT_BOOTSTRAP_FILENAME = "BOOTSTRAP.md";
23
- const TEMPLATE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../docs/reference/templates");
23
+ const TEMPLATE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../templates/customer/agents/admin");
24
24
  function stripFrontMatter(content) {
25
25
  if (!content.startsWith("---"))
26
26
  return content;
@@ -39,7 +39,7 @@ async function loadTemplate(name) {
39
39
  return stripFrontMatter(content);
40
40
  }
41
41
  catch {
42
- throw new Error(`Missing workspace template: ${name} (${templatePath}). Ensure docs/reference/templates are packaged.`);
42
+ throw new Error(`Missing workspace template: ${name} (${templatePath}). Ensure templates/customer is packaged.`);
43
43
  }
44
44
  }
45
45
  async function writeFileIfMissing(filePath, content) {
@@ -9,7 +9,6 @@ import { resolveGroupSessionKey, resolveSessionFilePath, } from "../../config/se
9
9
  import { logVerbose } from "../../globals.js";
10
10
  import { clearCommandLane, getQueueSize } from "../../process/command-queue.js";
11
11
  import { normalizeMainKey } from "../../routing/session-key.js";
12
- import { isReasoningTagProvider } from "../../utils/provider-utils.js";
13
12
  import { hasControlCommand } from "../command-detection.js";
14
13
  import { buildInboundMediaNote } from "../media-note.js";
15
14
  import { formatXHighModelHint, normalizeThinkLevel, supportsXHighThinking, } from "../thinking.js";
@@ -257,7 +256,7 @@ export async function runPreparedReply(params) {
257
256
  blockReplyBreak: resolvedBlockStreamingBreak,
258
257
  ownerNumbers: command.ownerList.length > 0 ? command.ownerList : undefined,
259
258
  extraSystemPrompt: extraSystemPrompt || undefined,
260
- ...(isReasoningTagProvider(provider) ? { enforceFinalTag: true } : {}),
259
+ enforceFinalTag: true,
261
260
  skipMessageDelivery: opts?.skipMessageDelivery,
262
261
  },
263
262
  };
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.10",
3
- "commit": "ea3f9738ae14b41c3edb34d5acc713e741b3f0db",
4
- "builtAt": "2026-02-15T09:22:34.386Z"
2
+ "version": "1.0.11",
3
+ "commit": "b6abbf7e2a7a62f677e6a5969f4d7c9054ad2894",
4
+ "builtAt": "2026-02-15T10:55:15.410Z"
5
5
  }
@@ -10,7 +10,7 @@ const DEV_IDENTITY_NAME = "C3-PO";
10
10
  const DEV_IDENTITY_THEME = "protocol droid";
11
11
  const DEV_IDENTITY_EMOJI = "🤖";
12
12
  const DEV_AGENT_WORKSPACE_SUFFIX = "dev";
13
- const DEV_TEMPLATE_DIR = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../../docs/reference/templates");
13
+ const DEV_TEMPLATE_DIR = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../../templates/customer/agents/admin");
14
14
  async function loadDevTemplate(name, fallback) {
15
15
  try {
16
16
  const raw = await fs.promises.readFile(path.join(DEV_TEMPLATE_DIR, name), "utf-8");
@@ -141,6 +141,7 @@ export function buildDefaultAgentList(workspaceRoot) {
141
141
  "list_admins",
142
142
  "relay_message",
143
143
  "browser",
144
+ "skill_read",
144
145
  ],
145
146
  deny: ["exec", "process", "group:fs", "group:runtime", "canvas"],
146
147
  },