@rubytech/taskmaster 1.14.2 → 1.16.1

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.
Files changed (47) hide show
  1. package/dist/agents/skills/frontmatter.js +1 -0
  2. package/dist/agents/skills/workspace.js +64 -22
  3. package/dist/agents/system-prompt.js +1 -1
  4. package/dist/agents/taskmaster-tools.js +6 -4
  5. package/dist/agents/tool-policy.js +2 -1
  6. package/dist/agents/tools/contact-create-tool.js +4 -3
  7. package/dist/agents/tools/contact-delete-tool.js +3 -2
  8. package/dist/agents/tools/contact-lookup-tool.js +5 -4
  9. package/dist/agents/tools/contact-update-tool.js +6 -3
  10. package/dist/agents/tools/memory-tool.js +3 -1
  11. package/dist/agents/tools/qr-generate-tool.js +45 -0
  12. package/dist/agents/workspace-migrations.js +351 -0
  13. package/dist/build-info.json +3 -3
  14. package/dist/config/agent-tools-reconcile.js +79 -0
  15. package/dist/control-ui/assets/{index-B3nkSwMP.js → index-Bd75cI7J.js} +547 -573
  16. package/dist/control-ui/assets/index-Bd75cI7J.js.map +1 -0
  17. package/dist/control-ui/assets/index-BkymP95Y.css +1 -0
  18. package/dist/control-ui/index.html +2 -2
  19. package/dist/gateway/server-http.js +5 -0
  20. package/dist/gateway/server-methods/web.js +13 -0
  21. package/dist/gateway/server.impl.js +29 -1
  22. package/dist/hooks/bundled/ride-dispatch/HOOK.md +57 -0
  23. package/dist/hooks/bundled/ride-dispatch/handler.js +450 -0
  24. package/dist/hooks/bundled/ride-dispatch/stripe-webhook.js +191 -0
  25. package/dist/memory/internal.js +24 -1
  26. package/dist/memory/manager.js +3 -3
  27. package/dist/records/records-manager.js +7 -2
  28. package/package.json +1 -1
  29. package/skills/business-assistant/SKILL.md +1 -1
  30. package/skills/qr-code/SKILL.md +63 -0
  31. package/skills/sales-closer/SKILL.md +1 -1
  32. package/templates/beagle-zanzibar/agents/admin/AGENTS.md +67 -1
  33. package/templates/beagle-zanzibar/agents/public/AGENTS.md +102 -22
  34. package/templates/beagle-zanzibar/skills/beagle-zanzibar/SKILL.md +7 -8
  35. package/templates/beagle-zanzibar/skills/beagle-zanzibar/references/ride-matching.md +46 -55
  36. package/templates/customer/agents/admin/BOOTSTRAP.md +5 -1
  37. package/templates/customer/agents/public/AGENTS.md +1 -2
  38. package/templates/real-agent/skills/buyer-feedback/SKILL.md +111 -0
  39. package/templates/real-agent/skills/property-enquiry/SKILL.md +126 -0
  40. package/templates/real-agent/skills/valuation-booking/SKILL.md +182 -0
  41. package/templates/real-agent/skills/vendor-updates/SKILL.md +153 -0
  42. package/templates/real-agent/skills/viewing-management/SKILL.md +111 -0
  43. package/templates/taskmaster/agents/public/AGENTS.md +1 -1
  44. package/templates/taskmaster/agents/public/IDENTITY.md +1 -1
  45. package/templates/taskmaster/agents/public/SOUL.md +2 -2
  46. package/dist/control-ui/assets/index-B3nkSwMP.js.map +0 -1
  47. package/dist/control-ui/assets/index-l54GcTyj.css +0 -1
@@ -90,6 +90,7 @@ export function resolveTaskmasterMetadata(frontmatter) {
90
90
  const osRaw = normalizeStringList(taskmasterObj.os);
91
91
  return {
92
92
  always: typeof taskmasterObj.always === "boolean" ? taskmasterObj.always : undefined,
93
+ embed: typeof taskmasterObj.embed === "boolean" ? taskmasterObj.embed : undefined,
93
94
  emoji: typeof taskmasterObj.emoji === "string" ? taskmasterObj.emoji : undefined,
94
95
  homepage: typeof taskmasterObj.homepage === "string" ? taskmasterObj.homepage : undefined,
95
96
  skillKey: typeof taskmasterObj.skillKey === "string" ? taskmasterObj.skillKey : undefined,
@@ -18,32 +18,73 @@ function escapeXml(str) {
18
18
  .replace(/"/g, """)
19
19
  .replace(/'/g, "'");
20
20
  }
21
+ /**
22
+ * Strip YAML frontmatter (between opening and closing `---`) from a skill
23
+ * file, returning only the body content.
24
+ */
25
+ function stripFrontmatter(content) {
26
+ const match = content.match(/^---\s*\n[\s\S]*?\n---\s*\n?/);
27
+ return match ? content.slice(match[0].length).trim() : content.trim();
28
+ }
21
29
  /**
22
30
  * Format skills for inclusion in a system prompt.
23
31
  * Identical to the library's formatSkillsForPrompt except it references
24
32
  * `skill_read` instead of `read` so agents denied group:fs can still
25
33
  * load skill content.
34
+ *
35
+ * Skills with `embed: true` metadata have their body content directly
36
+ * included in the system prompt instead of requiring `skill_read`.
26
37
  */
27
- function formatSkillsForPromptTaskmaster(skills) {
28
- const visibleSkills = skills.filter((s) => !s.disableModelInvocation);
29
- if (visibleSkills.length === 0)
38
+ function formatSkillsForPromptTaskmaster(entries) {
39
+ const visibleEntries = entries.filter((e) => !e.skill.disableModelInvocation);
40
+ if (visibleEntries.length === 0)
30
41
  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>");
42
+ // Partition into embedded and on-demand skills.
43
+ const embedded = [];
44
+ const onDemand = [];
45
+ for (const entry of visibleEntries) {
46
+ if (entry.taskmaster?.embed === true) {
47
+ embedded.push(entry);
48
+ }
49
+ else {
50
+ onDemand.push(entry);
51
+ }
52
+ }
53
+ const sections = [];
54
+ // Embedded skills: content included directly.
55
+ for (const entry of embedded) {
56
+ try {
57
+ const raw = fs.readFileSync(entry.skill.filePath, "utf-8");
58
+ const body = stripFrontmatter(raw);
59
+ if (body) {
60
+ sections.push(`<embedded_skill name="${escapeXml(entry.skill.name)}" location="${escapeXml(entry.skill.filePath)}">\n${body}\n</embedded_skill>`);
61
+ }
62
+ }
63
+ catch {
64
+ // If the file can't be read, fall through to on-demand.
65
+ onDemand.push(entry);
66
+ }
44
67
  }
45
- lines.push("</available_skills>");
46
- return lines.join("\n");
68
+ // On-demand skills: listed for skill_read loading.
69
+ if (onDemand.length > 0) {
70
+ const lines = [
71
+ "\n\nThe following skills provide specialized instructions for specific tasks.",
72
+ "Use the skill_read tool to load a skill's file when the task matches its description.",
73
+ "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.",
74
+ "",
75
+ "<available_skills>",
76
+ ];
77
+ for (const entry of onDemand) {
78
+ lines.push(" <skill>");
79
+ lines.push(` <name>${escapeXml(entry.skill.name)}</name>`);
80
+ lines.push(` <description>${escapeXml(entry.skill.description)}</description>`);
81
+ lines.push(` <location>${escapeXml(entry.skill.filePath)}</location>`);
82
+ lines.push(" </skill>");
83
+ }
84
+ lines.push("</available_skills>");
85
+ sections.push(lines.join("\n"));
86
+ }
87
+ return sections.join("\n\n");
47
88
  }
48
89
  const skillCommandDebugOnce = new Set();
49
90
  function debugSkillCommandOnce(messageKey, message, meta) {
@@ -176,7 +217,7 @@ export function buildWorkspaceSkillSnapshot(workspaceDir, opts) {
176
217
  const promptEntries = eligible.filter((entry) => entry.invocation?.disableModelInvocation !== true);
177
218
  const resolvedSkills = promptEntries.map((entry) => entry.skill);
178
219
  const remoteNote = opts?.eligibility?.remote?.note?.trim();
179
- const prompt = [remoteNote, formatSkillsForPromptTaskmaster(resolvedSkills)]
220
+ const prompt = [remoteNote, formatSkillsForPromptTaskmaster(promptEntries)]
180
221
  .filter(Boolean)
181
222
  .join("\n");
182
223
  return {
@@ -194,9 +235,7 @@ export function buildWorkspaceSkillsPrompt(workspaceDir, opts) {
194
235
  const eligible = filterSkillEntries(skillEntries, opts?.config, opts?.skillFilter, opts?.eligibility);
195
236
  const promptEntries = eligible.filter((entry) => entry.invocation?.disableModelInvocation !== true);
196
237
  const remoteNote = opts?.eligibility?.remote?.note?.trim();
197
- return [remoteNote, formatSkillsForPromptTaskmaster(promptEntries.map((entry) => entry.skill))]
198
- .filter(Boolean)
199
- .join("\n");
238
+ return [remoteNote, formatSkillsForPromptTaskmaster(promptEntries)].filter(Boolean).join("\n");
200
239
  }
201
240
  export function resolveSkillsPromptForRun(params) {
202
241
  const snapshotPrompt = params.skillsSnapshot?.prompt?.trim();
@@ -264,6 +303,9 @@ const FORCE_RESYNC_SKILLS = new Set([
264
303
  // v1.11.2: brevo skill updated with SMS credits reference; twilio scoped to voice-call only
265
304
  "brevo",
266
305
  "twilio",
306
+ // v1.14: sales-closer and business-assistant updated with embed:true
307
+ "sales-closer",
308
+ "business-assistant",
267
309
  ]);
268
310
  /**
269
311
  * Copy bundled skills into a workspace's `skills/` directory.
@@ -27,7 +27,7 @@ function buildMemorySection(params) {
27
27
  return [
28
28
  "## Memory Recall",
29
29
  "Memory is your knowledge base — customer profiles, business data, preferences, prior interactions, lessons, and instructions all live there. Proactively use memory_search whenever a topic might have stored context: who a person is, what was discussed before, business rules, pricing, product details, or anything that could have been recorded. Do not rely on conversation history alone — it only covers the current session. Use memory_get to pull specific lines when you know the file. If a search returns no results, say you checked.",
30
- "When skill files or workspace docs reference paths under `memory/` (e.g. `memory/public/data.md`), always access them with memory_get or memory_searchstrip the leading `memory/` prefix to get the relative path (e.g. `public/data.md`). Never use read or skill_read for memory content.",
30
+ "Paths for memory_get and memory_write accept either form: `memory/public/data.md` or `public/data.md` — the `memory/` prefix is added automatically when missing. Never use read or skill_read for memory content.",
31
31
  "",
32
32
  ];
33
33
  }
@@ -6,6 +6,7 @@ import { createCanvasTool } from "./tools/canvas-tool.js";
6
6
  import { createCronTool } from "./tools/cron-tool.js";
7
7
  import { createDocumentTool } from "./tools/document-tool.js";
8
8
  import { createDocumentToPdfTool } from "./tools/document-to-pdf-tool.js";
9
+ import { createQrGenerateTool } from "./tools/qr-generate-tool.js";
9
10
  import { createGatewayTool } from "./tools/gateway-tool.js";
10
11
  import { createImageTool } from "./tools/image-tool.js";
11
12
  import { createMemoryGetTool, createMemorySaveMediaTool, createMemorySearchTool, createMemoryWriteTool, } from "./tools/memory-tool.js";
@@ -148,16 +149,17 @@ export function createTaskmasterTools(options) {
148
149
  agentSessionKey: options?.agentSessionKey,
149
150
  sandboxRoot: options?.sandboxRoot,
150
151
  }),
152
+ createQrGenerateTool(),
151
153
  createCurrentTimeTool({ config: options?.config }),
152
154
  createAuthorizeAdminTool({ agentAccountId: options?.agentAccountId }),
153
155
  createRevokeAdminTool({ agentAccountId: options?.agentAccountId }),
154
156
  createListAdminsTool({ agentAccountId: options?.agentAccountId }),
155
157
  createBootstrapCompleteTool({ agentSessionKey: options?.agentSessionKey }),
156
158
  createLicenseGenerateTool(),
157
- createContactCreateTool(),
158
- createContactDeleteTool(),
159
- createContactLookupTool(),
160
- createContactUpdateTool(),
159
+ createContactCreateTool({ agentAccountId: options?.agentAccountId }),
160
+ createContactDeleteTool({ agentAccountId: options?.agentAccountId }),
161
+ createContactLookupTool({ agentAccountId: options?.agentAccountId }),
162
+ createContactUpdateTool({ agentAccountId: options?.agentAccountId }),
161
163
  createVerifyContactTool({ agentSessionKey: options?.agentSessionKey }),
162
164
  createVerifyContactCodeTool({ agentSessionKey: options?.agentSessionKey }),
163
165
  createRelayMessageTool(),
@@ -15,7 +15,7 @@ export const TOOL_GROUPS = {
15
15
  "message_history",
16
16
  "document_read",
17
17
  ],
18
- "group:documents": ["document_read", "document_to_pdf"],
18
+ "group:documents": ["document_read", "document_to_pdf", "qr_generate"],
19
19
  "group:web": ["web_search", "web_fetch"],
20
20
  // Time/date utilities
21
21
  "group:time": ["current_time"],
@@ -76,6 +76,7 @@ export const TOOL_GROUPS = {
76
76
  "message_history",
77
77
  "document_read",
78
78
  "document_to_pdf",
79
+ "qr_generate",
79
80
  "web_search",
80
81
  "web_fetch",
81
82
  "image",
@@ -20,7 +20,8 @@ const ContactCreateSchema = Type.Object({
20
20
  * are the canonical identifiers that link to user-scoped memory at
21
21
  * `memory/users/{phone}/`.
22
22
  */
23
- export function createContactCreateTool() {
23
+ export function createContactCreateTool(opts) {
24
+ const workspace = opts?.agentAccountId;
24
25
  return {
25
26
  label: "Contact Create",
26
27
  name: "contact_create",
@@ -42,13 +43,13 @@ export function createContactCreateTool() {
42
43
  if (!name) {
43
44
  return jsonResult({ error: "Name is required." });
44
45
  }
45
- const existing = getRecord(phone);
46
+ const existing = getRecord(phone, workspace);
46
47
  if (existing) {
47
48
  return jsonResult({
48
49
  error: `A contact record already exists for ${phone} (${existing.name}). Use contact_update to modify it.`,
49
50
  });
50
51
  }
51
- const record = setRecord(phone, { name, fields });
52
+ const record = setRecord(phone, { name, fields, workspace });
52
53
  return jsonResult({
53
54
  ok: true,
54
55
  action: "created",
@@ -13,7 +13,8 @@ const ContactDeleteSchema = Type.Object({
13
13
  * Does NOT delete associated user-scoped memory at `memory/users/{phone}/` —
14
14
  * that is a separate decision for the business owner.
15
15
  */
16
- export function createContactDeleteTool() {
16
+ export function createContactDeleteTool(opts) {
17
+ const workspace = opts?.agentAccountId;
17
18
  return {
18
19
  label: "Contact Delete",
19
20
  name: "contact_delete",
@@ -27,7 +28,7 @@ export function createContactDeleteTool() {
27
28
  if (!phone) {
28
29
  return jsonResult({ error: "Phone number is required." });
29
30
  }
30
- const existing = getRecord(phone);
31
+ const existing = getRecord(phone, workspace);
31
32
  if (!existing) {
32
33
  return jsonResult({
33
34
  error: `No contact record found for ${phone}.`,
@@ -9,7 +9,8 @@ const ContactLookupSchema = Type.Object({
9
9
  description: "Name to search for (partial match).",
10
10
  })),
11
11
  });
12
- export function createContactLookupTool() {
12
+ export function createContactLookupTool(opts) {
13
+ const workspace = opts?.agentAccountId;
13
14
  return {
14
15
  label: "Contact Lookup",
15
16
  name: "contact_lookup",
@@ -22,20 +23,20 @@ export function createContactLookupTool() {
22
23
  const phone = typeof params.phone === "string" ? params.phone.trim() : undefined;
23
24
  const name = typeof params.name === "string" ? params.name.trim() : undefined;
24
25
  if (!phone && !name) {
25
- const all = listRecords();
26
+ const all = listRecords(workspace);
26
27
  if (all.length === 0) {
27
28
  return jsonResult({ found: false, message: "No contact records exist yet." });
28
29
  }
29
30
  return jsonResult({ found: true, count: all.length, records: all });
30
31
  }
31
32
  if (phone) {
32
- const record = getRecord(phone);
33
+ const record = getRecord(phone, workspace);
33
34
  if (!record) {
34
35
  return jsonResult({ found: false, message: `No record found for ${phone}.` });
35
36
  }
36
37
  return jsonResult({ found: true, record });
37
38
  }
38
- const results = searchRecords(name);
39
+ const results = searchRecords(name, workspace);
39
40
  if (results.length === 0) {
40
41
  return jsonResult({ found: false, message: `No records matching "${name}".` });
41
42
  }
@@ -20,7 +20,8 @@ const ContactUpdateSchema = Type.Object({
20
20
  * The public agent does NOT have access to this tool (same gating as
21
21
  * license_generate — not in any profile allow list).
22
22
  */
23
- export function createContactUpdateTool() {
23
+ export function createContactUpdateTool(opts) {
24
+ const workspace = opts?.agentAccountId;
24
25
  return {
25
26
  label: "Contact Update",
26
27
  name: "contact_update",
@@ -39,7 +40,7 @@ export function createContactUpdateTool() {
39
40
  if (!field) {
40
41
  return jsonResult({ error: "Field name is required." });
41
42
  }
42
- const existing = getRecord(phone);
43
+ const existing = getRecord(phone, workspace);
43
44
  if (!existing) {
44
45
  return jsonResult({
45
46
  error: `No contact record found for ${phone}. Create one via the Contacts admin page first.`,
@@ -48,7 +49,9 @@ export function createContactUpdateTool() {
48
49
  // "name" updates the contact's canonical display name, not a custom field.
49
50
  if (field === "name") {
50
51
  if (value === undefined) {
51
- return jsonResult({ error: "Cannot delete the contact name. Provide a new value instead." });
52
+ return jsonResult({
53
+ error: "Cannot delete the contact name. Provide a new value instead.",
54
+ });
52
55
  }
53
56
  const updated = setRecordName(phone, value);
54
57
  return jsonResult({
@@ -9,7 +9,9 @@ const MemorySearchSchema = Type.Object({
9
9
  minScore: Type.Optional(Type.Number()),
10
10
  });
11
11
  const MemoryGetSchema = Type.Object({
12
- path: Type.String(),
12
+ path: Type.String({
13
+ description: 'Path within memory, e.g. "memory/public/data.md" or "public/data.md"',
14
+ }),
13
15
  from: Type.Optional(Type.Number()),
14
16
  lines: Type.Optional(Type.Number()),
15
17
  });
@@ -0,0 +1,45 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { Type } from "@sinclair/typebox";
5
+ import { renderQrPngBase64 } from "../../web/qr-image.js";
6
+ import { jsonResult, readStringParam } from "./common.js";
7
+ const QrGenerateSchema = Type.Object({
8
+ data: Type.String({
9
+ description: "The content to encode as a QR code. Pass the final string directly: " +
10
+ "a URL, plain text, a vCard block, or a wa.me deep link.",
11
+ }),
12
+ });
13
+ export function createQrGenerateTool() {
14
+ return {
15
+ label: "QR Code Generator",
16
+ name: "qr_generate",
17
+ description: "Generate a QR code PNG from any string (URL, text, vCard, WhatsApp link). " +
18
+ "Returns a MEDIA: path — copy it into the message tool's media parameter to send the image.",
19
+ parameters: QrGenerateSchema,
20
+ execute: async (_toolCallId, params) => {
21
+ const data = readStringParam(params, "data");
22
+ if (!data) {
23
+ return jsonResult({ ok: false, error: "data is empty — provide the content to encode" });
24
+ }
25
+ try {
26
+ const base64 = await renderQrPngBase64(data);
27
+ const buf = Buffer.from(base64, "base64");
28
+ const filename = `qr-${Date.now()}.png`;
29
+ const pngPath = path.join(os.tmpdir(), filename);
30
+ await fs.writeFile(pngPath, buf);
31
+ return {
32
+ content: [
33
+ { type: "text", text: `MEDIA:${pngPath}` },
34
+ { type: "text", text: JSON.stringify({ ok: true, path: pngPath }) },
35
+ ],
36
+ details: { ok: true, path: pngPath },
37
+ };
38
+ }
39
+ catch (err) {
40
+ const message = err instanceof Error ? err.message : String(err);
41
+ return jsonResult({ ok: false, error: `QR generation failed: ${message}` });
42
+ }
43
+ },
44
+ };
45
+ }