@rubytech/taskmaster 1.42.0 → 1.42.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 (41) hide show
  1. package/dist/agents/context.js +2 -0
  2. package/dist/agents/pi-embedded-runner/compact.js +1 -0
  3. package/dist/agents/pi-embedded-runner/run/attempt.js +1 -0
  4. package/dist/agents/pi-embedded-runner/system-prompt.js +1 -0
  5. package/dist/agents/skills/frontmatter.js +85 -1
  6. package/dist/agents/skills/workspace.js +23 -2
  7. package/dist/agents/skills-status.js +2 -2
  8. package/dist/agents/system-prompt.js +43 -30
  9. package/dist/agents/tool-policy.js +68 -4
  10. package/dist/agents/tools/access-manage-tool.js +110 -0
  11. package/dist/agents/tools/account-manage-tool.js +78 -0
  12. package/dist/agents/tools/brand-settings-tool.js +53 -24
  13. package/dist/agents/tools/file-manage-tool.js +193 -0
  14. package/dist/agents/tools/license-manage-tool.js +50 -0
  15. package/dist/agents/tools/skill-manage-tool.js +16 -3
  16. package/dist/auto-reply/reply/commands-context-report.js +2 -1
  17. package/dist/browser/chrome.js +11 -0
  18. package/dist/build-info.json +3 -3
  19. package/dist/cli/skills-cli.js +4 -9
  20. package/dist/commands/onboard-skills.js +1 -1
  21. package/dist/config/zod-schema.js +5 -1
  22. package/dist/control-ui/assets/index-CAu2PL0O.css +1 -0
  23. package/dist/control-ui/assets/{index-Cl91wvkO.js → index-c6Rca5oP.js} +691 -542
  24. package/dist/control-ui/assets/index-c6Rca5oP.js.map +1 -0
  25. package/dist/control-ui/index.html +3 -2
  26. package/dist/gateway/protocol/index.js +3 -2
  27. package/dist/gateway/protocol/schema/agents-models-skills.js +6 -1
  28. package/dist/gateway/protocol/schema/tools.js +8 -0
  29. package/dist/gateway/server-methods/sessions.js +3 -1
  30. package/dist/gateway/server-methods/skills.js +31 -2
  31. package/dist/gateway/server-methods/tools.js +7 -0
  32. package/dist/gateway/server-methods.js +3 -0
  33. package/dist/gateway/session-utils.js +5 -1
  34. package/dist/media-understanding/apply.js +18 -4
  35. package/dist/web/auto-reply/monitor/process-message.js +1 -0
  36. package/dist/web/inbound/monitor.js +4 -0
  37. package/package.json +1 -1
  38. package/skills/skill-builder/SKILL.md +18 -4
  39. package/skills/skill-builder/references/lean-pattern.md +13 -3
  40. package/dist/control-ui/assets/index-Cl91wvkO.js.map +0 -1
  41. package/dist/control-ui/assets/index-Dd2cHcuh.css +0 -1
@@ -2,45 +2,52 @@
2
2
  * Agent tool for managing branding settings.
3
3
  *
4
4
  * Wraps `config.get`/`config.patch` for accent/background color and
5
- * `brand.hasLogo`/`brand.removeLogo` for logo management. Logo upload
6
- * is excluded — it requires binary data better handled through the UI.
5
+ * `brand.hasLogo`/`brand.removeLogo`/`brand.uploadLogo` for logo management.
7
6
  */
8
7
  import { Type } from "@sinclair/typebox";
9
8
  import { stringEnum } from "../schema/typebox.js";
10
9
  import { jsonResult, readStringParam } from "./common.js";
11
10
  import { callGatewayTool } from "./gateway.js";
12
- const BRAND_ACTIONS = ["get", "set_colors", "remove_logo"];
11
+ const BRAND_ACTIONS = ["get", "set_colors", "set_logo", "remove_logo"];
13
12
  const BrandSettingsSchema = Type.Object({
14
13
  action: stringEnum(BRAND_ACTIONS, {
15
14
  description: '"get" returns current branding (accent color, background color, logo status). ' +
16
15
  '"set_colors" updates accent and/or background colors. ' +
16
+ '"set_logo" uploads a workspace logo from base64 data. ' +
17
17
  '"remove_logo" removes the workspace logo.',
18
18
  }),
19
- workspace: Type.Optional(Type.String({
20
- description: "Workspace name. Required for logo operations. Defaults to the active workspace.",
21
- })),
19
+ workspace: Type.String({
20
+ description: "Workspace name (e.g. \"taskmaster\"). Required for all actions.",
21
+ }),
22
22
  accentColor: Type.Optional(Type.String({
23
23
  description: 'Hex color for the accent/primary color (e.g. "#3B82F6"). Used with set_colors.',
24
24
  })),
25
25
  backgroundColor: Type.Optional(Type.String({
26
26
  description: 'Hex color for the background (e.g. "#1A1A2E"). Used with set_colors.',
27
27
  })),
28
+ imageData: Type.Optional(Type.String({
29
+ description: "Base64-encoded image data for the logo. Required for set_logo.",
30
+ })),
31
+ mimeType: Type.Optional(Type.String({
32
+ description: 'MIME type of the logo image (e.g. "image/png", "image/svg+xml"). Required for set_logo.',
33
+ })),
28
34
  });
29
35
  export function createBrandSettingsTool() {
30
36
  return {
31
37
  label: "Brand Settings",
32
38
  name: "brand_settings",
33
- description: "Read and update branding settings. " +
39
+ description: "Read and update workspace branding settings. Workspace name is required for all actions. " +
34
40
  '"get" returns current accent color, background color, and logo status. ' +
35
41
  '"set_colors" updates accent and/or background colors (hex values). ' +
36
- '"remove_logo" removes the workspace logo. ' +
37
- "Logo upload requires the UI due to binary data — suggest the user upload through the Setup page.",
42
+ '"set_logo" uploads a logo from base64-encoded image data (imageData + mimeType required). ' +
43
+ '"remove_logo" removes the workspace logo.',
38
44
  parameters: BrandSettingsSchema,
39
45
  execute: async (_toolCallId, args) => {
40
46
  const params = args;
41
47
  const action = readStringParam(params, "action", { required: true });
42
48
  const gatewayOpts = {};
43
49
  if (action === "get") {
50
+ const workspace = readStringParam(params, "workspace", { required: true, label: "workspace" });
44
51
  const config = await callGatewayTool("config.get", gatewayOpts, {});
45
52
  let brand = {};
46
53
  if (config &&
@@ -48,23 +55,20 @@ export function createBrandSettingsTool() {
48
55
  typeof config.raw === "string") {
49
56
  try {
50
57
  const parsed = JSON.parse(config.raw);
51
- brand = parsed?.brand ?? {};
58
+ brand = parsed?.workspaces?.[workspace]?.brand ?? {};
52
59
  }
53
60
  catch {
54
61
  // fallback to empty
55
62
  }
56
63
  }
57
64
  // Also check logo status
58
- const workspace = readStringParam(params, "workspace");
59
65
  let hasLogo;
60
- if (workspace) {
61
- try {
62
- const logoResult = await callGatewayTool("brand.hasLogo", gatewayOpts, { workspace });
63
- hasLogo = logoResult?.hasLogo;
64
- }
65
- catch {
66
- // non-critical
67
- }
66
+ try {
67
+ const logoResult = await callGatewayTool("brand.hasLogo", gatewayOpts, { workspace });
68
+ hasLogo = logoResult?.hasLogo;
69
+ }
70
+ catch {
71
+ // non-critical
68
72
  }
69
73
  return jsonResult({
70
74
  accentColor: brand.accentColor ?? null,
@@ -73,6 +77,7 @@ export function createBrandSettingsTool() {
73
77
  });
74
78
  }
75
79
  if (action === "set_colors") {
80
+ const workspace = readStringParam(params, "workspace", { required: true, label: "workspace" });
76
81
  const accentColor = readStringParam(params, "accentColor");
77
82
  const backgroundColor = readStringParam(params, "backgroundColor");
78
83
  if (!accentColor && !backgroundColor) {
@@ -85,18 +90,42 @@ export function createBrandSettingsTool() {
85
90
  brandPatch.backgroundColor = backgroundColor;
86
91
  const snapshot = await callGatewayTool("config.get", gatewayOpts, {});
87
92
  const baseHash = typeof snapshot?.hash === "string" ? snapshot.hash : undefined;
93
+ // Preserve existing workspace config, only updating brand colors
94
+ let existingWorkspace = {};
95
+ if (typeof snapshot?.raw === "string") {
96
+ try {
97
+ const parsed = JSON.parse(snapshot.raw);
98
+ existingWorkspace = parsed?.workspaces?.[workspace] ?? {};
99
+ }
100
+ catch {
101
+ // fallback to empty
102
+ }
103
+ }
104
+ const existingBrand = existingWorkspace.brand ?? {};
88
105
  const result = await callGatewayTool("config.patch", gatewayOpts, {
89
- raw: JSON.stringify({ brand: brandPatch }),
106
+ raw: JSON.stringify({
107
+ workspaces: {
108
+ [workspace]: { ...existingWorkspace, brand: { ...existingBrand, ...brandPatch } },
109
+ },
110
+ }),
90
111
  baseHash,
91
112
  note: "agent: update brand colors",
92
113
  });
93
114
  return jsonResult({ ok: true, ...brandPatch, result });
94
115
  }
95
- if (action === "remove_logo") {
96
- const workspace = readStringParam(params, "workspace", {
97
- required: true,
98
- label: "workspace",
116
+ if (action === "set_logo") {
117
+ const workspace = readStringParam(params, "workspace", { required: true, label: "workspace" });
118
+ const imageData = readStringParam(params, "imageData", { required: true, label: "imageData" });
119
+ const mimeType = readStringParam(params, "mimeType", { required: true, label: "mimeType" });
120
+ const result = await callGatewayTool("brand.uploadLogo", gatewayOpts, {
121
+ workspace,
122
+ data: imageData,
123
+ mimeType,
99
124
  });
125
+ return jsonResult(result);
126
+ }
127
+ if (action === "remove_logo") {
128
+ const workspace = readStringParam(params, "workspace", { required: true, label: "workspace" });
100
129
  const result = await callGatewayTool("brand.removeLogo", gatewayOpts, { workspace });
101
130
  return jsonResult(result);
102
131
  }
@@ -0,0 +1,193 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { realpath } from "node:fs/promises";
4
+ import { Type } from "@sinclair/typebox";
5
+ import { resolveAgentWorkspaceRoot, resolveSessionAgentId } from "../agent-scope.js";
6
+ import { stringEnum } from "../schema/typebox.js";
7
+ import { jsonResult, readStringParam } from "./common.js";
8
+ const FileManageSchema = Type.Object({
9
+ action: stringEnum(["mkdir", "move"], {
10
+ description: "The operation to perform: 'mkdir' to create a directory, 'move' to move/rename a file or directory.",
11
+ }),
12
+ path: Type.String({
13
+ description: "Target path relative to the workspace root " +
14
+ "(e.g. 'uploads/invoices', 'memory/users/scratch.md').",
15
+ }),
16
+ destination: Type.Optional(Type.String({
17
+ description: "Destination path relative to the workspace root (required for 'move').",
18
+ })),
19
+ });
20
+ /**
21
+ * Protected top-level entries that cannot be created over or moved.
22
+ * These are structural directories/files whose modification would break the workspace.
23
+ */
24
+ const PROTECTED_TOP_LEVEL = new Set(["agents", "IDENTITY.md", "SOUL.md", "AGENTS.md"]);
25
+ /**
26
+ * Validate that `target` is strictly inside `root` (not equal to root).
27
+ * Both paths must be real (symlinks resolved) before calling.
28
+ */
29
+ function isInsideRoot(target, root) {
30
+ const prefix = root.endsWith("/") ? root : `${root}/`;
31
+ return target.startsWith(prefix);
32
+ }
33
+ /**
34
+ * Check whether a resolved path's top-level segment is a protected entry.
35
+ */
36
+ function isProtectedPath(resolved, root) {
37
+ const relative = path.relative(root, resolved);
38
+ const topSegment = relative.split(path.sep)[0];
39
+ if (topSegment && PROTECTED_TOP_LEVEL.has(topSegment)) {
40
+ return topSegment;
41
+ }
42
+ return null;
43
+ }
44
+ export function createFileManageTool(options) {
45
+ return {
46
+ label: "File Management",
47
+ name: "file_manage",
48
+ description: "Create directories or move/rename files and folders in the workspace. " +
49
+ "All paths are relative to the workspace root. " +
50
+ "Cannot modify protected structural paths (agents/, IDENTITY.md, SOUL.md, etc.).",
51
+ parameters: FileManageSchema,
52
+ execute: async (_toolCallId, args) => {
53
+ const params = args;
54
+ const action = readStringParam(params, "action", { required: true });
55
+ const rawPath = readStringParam(params, "path", { required: true });
56
+ // ── Resolve workspace root ──────────────────────────────────
57
+ const cfg = options?.config;
58
+ if (!cfg) {
59
+ return jsonResult({ error: "No config available — cannot resolve workspace." });
60
+ }
61
+ const agentId = resolveSessionAgentId({
62
+ sessionKey: options?.agentSessionKey,
63
+ config: cfg,
64
+ });
65
+ const workspaceRoot = resolveAgentWorkspaceRoot(cfg, agentId);
66
+ // ── Validate path ───────────────────────────────────────────
67
+ if (rawPath.includes("\0")) {
68
+ return jsonResult({ error: "Invalid path." });
69
+ }
70
+ const resolved = path.resolve(workspaceRoot, rawPath);
71
+ // ── Containment check (pre-realpath) ────────────────────────
72
+ if (!isInsideRoot(resolved, workspaceRoot)) {
73
+ return jsonResult({ error: "Path is outside the workspace." });
74
+ }
75
+ // ── Resolve real workspace root for post-realpath checks ───
76
+ let realRoot;
77
+ try {
78
+ realRoot = await realpath(workspaceRoot);
79
+ }
80
+ catch {
81
+ return jsonResult({ error: "Workspace root is not accessible." });
82
+ }
83
+ // ── mkdir ─────────────────────────────────────────────────
84
+ if (action === "mkdir") {
85
+ // Protected path check
86
+ const protectedSegment = isProtectedPath(resolved, workspaceRoot);
87
+ if (protectedSegment) {
88
+ return jsonResult({
89
+ error: `Cannot create directory over protected path: ${protectedSegment} is a structural workspace entry.`,
90
+ });
91
+ }
92
+ try {
93
+ await fs.mkdir(resolved, { recursive: true });
94
+ }
95
+ catch (err) {
96
+ const message = err instanceof Error ? err.message : String(err);
97
+ return jsonResult({ error: `Failed to create directory: ${message}` });
98
+ }
99
+ // Post-creation containment check (resolves any symlinks created during mkdir)
100
+ let realTarget;
101
+ try {
102
+ realTarget = await realpath(resolved);
103
+ }
104
+ catch {
105
+ realTarget = resolved;
106
+ }
107
+ if (!isInsideRoot(realTarget, realRoot)) {
108
+ // Roll back — the created directory escapes the workspace
109
+ try {
110
+ await fs.rm(resolved, { recursive: true });
111
+ }
112
+ catch {
113
+ // best-effort cleanup
114
+ }
115
+ return jsonResult({ error: "Path resolves outside the workspace." });
116
+ }
117
+ return jsonResult({
118
+ ok: true,
119
+ action: "mkdir",
120
+ path: rawPath,
121
+ });
122
+ }
123
+ // ── move ──────────────────────────────────────────────────
124
+ if (action === "move") {
125
+ const rawDestination = readStringParam(params, "destination");
126
+ if (!rawDestination) {
127
+ return jsonResult({ error: "The 'destination' parameter is required for the 'move' action." });
128
+ }
129
+ if (rawDestination.includes("\0")) {
130
+ return jsonResult({ error: "Invalid destination path." });
131
+ }
132
+ const resolvedDest = path.resolve(workspaceRoot, rawDestination);
133
+ // Containment check for destination (pre-realpath)
134
+ if (!isInsideRoot(resolvedDest, workspaceRoot)) {
135
+ return jsonResult({ error: "Destination is outside the workspace." });
136
+ }
137
+ // Check source exists
138
+ try {
139
+ await fs.lstat(resolved);
140
+ }
141
+ catch {
142
+ return jsonResult({ error: `Not found: ${rawPath}` });
143
+ }
144
+ // Post-realpath containment check for source
145
+ let realSource;
146
+ try {
147
+ realSource = await realpath(resolved);
148
+ }
149
+ catch {
150
+ realSource = resolved;
151
+ }
152
+ if (!isInsideRoot(realSource, realRoot)) {
153
+ return jsonResult({ error: "Source path resolves outside the workspace." });
154
+ }
155
+ // Protected path check — cannot move protected entries
156
+ const protectedSource = isProtectedPath(realSource, realRoot);
157
+ if (protectedSource) {
158
+ return jsonResult({
159
+ error: `Cannot move protected path: ${protectedSource} is a structural workspace entry.`,
160
+ });
161
+ }
162
+ const protectedDest = isProtectedPath(resolvedDest, workspaceRoot);
163
+ if (protectedDest) {
164
+ return jsonResult({
165
+ error: `Cannot move to protected path: ${protectedDest} is a structural workspace entry.`,
166
+ });
167
+ }
168
+ // Ensure destination parent directory exists
169
+ const destParent = path.dirname(resolvedDest);
170
+ try {
171
+ await fs.mkdir(destParent, { recursive: true });
172
+ }
173
+ catch {
174
+ // parent may already exist — ignore
175
+ }
176
+ try {
177
+ await fs.rename(resolved, resolvedDest);
178
+ }
179
+ catch (err) {
180
+ const message = err instanceof Error ? err.message : String(err);
181
+ return jsonResult({ error: `Failed to move: ${message}` });
182
+ }
183
+ return jsonResult({
184
+ ok: true,
185
+ action: "move",
186
+ from: rawPath,
187
+ to: rawDestination,
188
+ });
189
+ }
190
+ return jsonResult({ error: `Unknown action: ${action}` });
191
+ },
192
+ };
193
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Agent tool for managing license activation and removal.
3
+ *
4
+ * Wraps the gateway `license.status`, `license.activate`, and `license.remove`
5
+ * RPC methods so the agent can check license state, activate a new license
6
+ * token, or clear the stored license — without asking the user to open the
7
+ * Setup UI.
8
+ */
9
+ import { Type } from "@sinclair/typebox";
10
+ import { stringEnum } from "../schema/typebox.js";
11
+ import { jsonResult, readStringParam } from "./common.js";
12
+ import { callGatewayTool } from "./gateway.js";
13
+ const LICENSE_ACTIONS = ["status", "activate", "remove"];
14
+ const LicenseManageSchema = Type.Object({
15
+ action: stringEnum(LICENSE_ACTIONS, {
16
+ description: 'Action to perform: "status" shows current license info and device ID, "activate" validates and stores a license token, "remove" clears the stored license (e.g. for transferring to another device).',
17
+ }),
18
+ key: Type.Optional(Type.String({
19
+ description: "The license token string (required for activate).",
20
+ })),
21
+ });
22
+ export function createLicenseManageTool() {
23
+ return {
24
+ label: "License Management",
25
+ name: "license_manage",
26
+ description: "Manage the device license. Use action 'status' to show current license info and device ID, 'activate' to validate and store a license token (key required), or 'remove' to clear the stored license (e.g. for transferring to another device).",
27
+ parameters: LicenseManageSchema,
28
+ execute: async (_toolCallId, args) => {
29
+ const params = args;
30
+ const action = readStringParam(params, "action", { required: true });
31
+ switch (action) {
32
+ case "status": {
33
+ const result = await callGatewayTool("license.status", {});
34
+ return jsonResult(result);
35
+ }
36
+ case "activate": {
37
+ const key = readStringParam(params, "key", { required: true });
38
+ const result = await callGatewayTool("license.activate", {}, { key });
39
+ return jsonResult(result);
40
+ }
41
+ case "remove": {
42
+ const result = await callGatewayTool("license.remove", {});
43
+ return jsonResult(result);
44
+ }
45
+ default:
46
+ throw new Error(`Unknown action: ${action}. Use "status", "activate", or "remove".`);
47
+ }
48
+ },
49
+ };
50
+ }
@@ -9,14 +9,16 @@ import { Type } from "@sinclair/typebox";
9
9
  import { stringEnum } from "../schema/typebox.js";
10
10
  import { jsonResult, readStringParam } from "./common.js";
11
11
  import { callGatewayTool } from "./gateway.js";
12
- const SKILL_MANAGE_ACTIONS = ["list", "create", "update", "delete", "install"];
12
+ const SKILL_MANAGE_ACTIONS = ["list", "create", "update", "delete", "install", "list_drafts", "delete_draft"];
13
13
  const SkillManageSchema = Type.Object({
14
14
  action: stringEnum(SKILL_MANAGE_ACTIONS, {
15
15
  description: '"list" returns all skills with their status. ' +
16
16
  '"create" creates a new user skill (name + skillContent required). ' +
17
17
  '"update" modifies a skill setting (skillKey required, plus enabled/apiKey/env). ' +
18
18
  '"delete" removes a user skill (name required; preloaded skills cannot be deleted). ' +
19
- '"install" installs a skill from a remote source (name + installId required).',
19
+ '"install" installs a skill from a remote source (name + installId required). ' +
20
+ '"list_drafts" returns all saved skill drafts. ' +
21
+ '"delete_draft" removes a saved draft (name required).',
20
22
  }),
21
23
  name: Type.Optional(Type.String({
22
24
  description: "Skill name/key (required for create, delete, install).",
@@ -50,7 +52,9 @@ export function createSkillManageTool() {
50
52
  '"create" makes a new skill (provide name + skillContent with YAML frontmatter). ' +
51
53
  '"update" modifies settings (enabled, apiKey). ' +
52
54
  '"delete" removes a user skill (preloaded skills are protected). ' +
53
- '"install" adds a skill from a remote source.',
55
+ '"install" adds a skill from a remote source. ' +
56
+ '"list_drafts" shows all saved skill drafts. ' +
57
+ '"delete_draft" removes a saved draft (name required).',
54
58
  parameters: SkillManageSchema,
55
59
  execute: async (_toolCallId, args) => {
56
60
  const params = args;
@@ -99,6 +103,15 @@ export function createSkillManageTool() {
99
103
  const result = await callGatewayTool("skills.install", {}, { name, installId });
100
104
  return jsonResult(result);
101
105
  }
106
+ if (action === "list_drafts") {
107
+ const result = await callGatewayTool("skills.drafts", {}, {});
108
+ return jsonResult(result);
109
+ }
110
+ if (action === "delete_draft") {
111
+ const name = readStringParam(params, "name", { required: true, label: "name" });
112
+ const result = await callGatewayTool("skills.deleteDraft", {}, { name });
113
+ return jsonResult(result);
114
+ }
102
115
  throw new Error(`Unknown action: ${action}`);
103
116
  },
104
117
  };
@@ -56,7 +56,7 @@ async function resolveContextReport(params) {
56
56
  });
57
57
  }
58
58
  catch {
59
- return { prompt: "", skills: [], resolvedSkills: [] };
59
+ return { prompt: "", skills: [], resolvedSkills: [], skillSpawnEntries: [] };
60
60
  }
61
61
  })();
62
62
  const skillsPrompt = skillsSnapshot.prompt ?? "";
@@ -135,6 +135,7 @@ async function resolveContextReport(params) {
135
135
  userTimeFormat,
136
136
  contextFiles: injectedFiles,
137
137
  skillsPrompt,
138
+ skillSpawnEntries: skillsSnapshot.skillSpawnEntries,
138
139
  heartbeatPrompt: undefined,
139
140
  ttsHint,
140
141
  runtimeInfo,
@@ -219,6 +219,17 @@ export async function launchTaskmasterChrome(resolved, profile) {
219
219
  catch (err) {
220
220
  log.warn(`Taskmaster browser clean-exit prefs failed: ${String(err)}`);
221
221
  }
222
+ // Remove stale Chromium singleton lock files before launch. These are left
223
+ // behind after a crash or when the hostname changes (SingletonSocket encodes
224
+ // the hostname). If present, Chromium refuses to start.
225
+ for (const name of ["SingletonLock", "SingletonCookie", "SingletonSocket"]) {
226
+ try {
227
+ fs.rmSync(path.join(userDataDir, name), { force: true });
228
+ }
229
+ catch {
230
+ // ignore — file may not exist
231
+ }
232
+ }
222
233
  const proc = spawnOnce();
223
234
  // Wait for CDP to come up.
224
235
  const readyDeadline = Date.now() + 15_000;
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.42.0",
3
- "commit": "a5066df83a7f83107c9e714922f980e35bf44a0e",
4
- "builtAt": "2026-03-13T17:11:29.024Z"
2
+ "version": "1.42.1",
3
+ "commit": "d860a07a6a2c8b8db885801f38ec7d4d5503d1e0",
4
+ "builtAt": "2026-03-14T21:34:46.499Z"
5
5
  }
@@ -22,8 +22,7 @@ function formatSkillStatus(skill) {
22
22
  return theme.error("✗ missing");
23
23
  }
24
24
  function formatSkillName(skill) {
25
- const emoji = skill.emoji ?? "📦";
26
- return `${emoji} ${theme.command(skill.name)}`;
25
+ return `🧩 ${theme.command(skill.name)}`;
27
26
  }
28
27
  function formatSkillMissingSummary(skill) {
29
28
  const missing = [];
@@ -56,7 +55,6 @@ export function formatSkillsList(report, opts) {
56
55
  skills: skills.map((s) => ({
57
56
  name: s.name,
58
57
  description: s.description,
59
- emoji: s.emoji,
60
58
  eligible: s.eligible,
61
59
  disabled: s.disabled,
62
60
  blockedByAllowlist: s.blockedByAllowlist,
@@ -119,7 +117,6 @@ export function formatSkillInfo(report, skillName, opts) {
119
117
  return JSON.stringify(skill, null, 2);
120
118
  }
121
119
  const lines = [];
122
- const emoji = skill.emoji ?? "📦";
123
120
  const status = skill.eligible
124
121
  ? theme.success("✓ Ready")
125
122
  : skill.disabled
@@ -127,7 +124,7 @@ export function formatSkillInfo(report, skillName, opts) {
127
124
  : skill.blockedByAllowlist
128
125
  ? theme.warn("🚫 Blocked by allowlist")
129
126
  : theme.error("✗ Missing requirements");
130
- lines.push(`${emoji} ${theme.heading(skill.name)} ${status}`);
127
+ lines.push(`🧩 ${theme.heading(skill.name)} ${status}`);
131
128
  lines.push("");
132
129
  lines.push(skill.description);
133
130
  lines.push("");
@@ -236,15 +233,13 @@ export function formatSkillsCheck(report, opts) {
236
233
  lines.push("");
237
234
  lines.push(theme.heading("Ready to use:"));
238
235
  for (const skill of eligible) {
239
- const emoji = skill.emoji ?? "📦";
240
- lines.push(` ${emoji} ${skill.name}`);
236
+ lines.push(` 🧩 ${skill.name}`);
241
237
  }
242
238
  }
243
239
  if (missingReqs.length > 0) {
244
240
  lines.push("");
245
241
  lines.push(theme.heading("Missing requirements:"));
246
242
  for (const skill of missingReqs) {
247
- const emoji = skill.emoji ?? "📦";
248
243
  const missing = [];
249
244
  if (skill.missing.bins.length > 0) {
250
245
  missing.push(`bins: ${skill.missing.bins.join(", ")}`);
@@ -261,7 +256,7 @@ export function formatSkillsCheck(report, opts) {
261
256
  if (skill.missing.os.length > 0) {
262
257
  missing.push(`os: ${skill.missing.os.join(", ")}`);
263
258
  }
264
- lines.push(` ${emoji} ${skill.name} ${theme.muted(`(${missing.join("; ")})`)}`);
259
+ lines.push(` 🧩 ${skill.name} ${theme.muted(`(${missing.join("; ")})`)}`);
265
260
  }
266
261
  }
267
262
  return appendTaskmasterHubHint(lines.join("\n"), opts.json);
@@ -91,7 +91,7 @@ export async function setupSkills(cfg, workspaceDir, runtime, prompter) {
91
91
  },
92
92
  ...installable.map((skill) => ({
93
93
  value: skill.name,
94
- label: `${skill.emoji ?? "🧩"} ${skill.name}`,
94
+ label: `🧩 ${skill.name}`,
95
95
  hint: formatSkillHint(skill),
96
96
  })),
97
97
  ],
@@ -514,8 +514,12 @@ export const TaskmasterSchema = z
514
514
  apiKey: z.string().optional(),
515
515
  env: z.record(z.string(), z.string()).optional(),
516
516
  config: z.record(z.string(), z.unknown()).optional(),
517
+ agents: z
518
+ .array(z.union([z.literal("admin"), z.literal("public"), z.literal("subagent")]))
519
+ .optional(),
517
520
  })
518
- .strict())
521
+ .strict()
522
+ .refine((val) => !val.agents?.includes("subagent") || val.agents.length === 1, { message: '"subagent" must be the sole value in agents' }))
519
523
  .optional(),
520
524
  })
521
525
  .strict()