@rubytech/taskmaster 1.0.106 → 1.0.108

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 (62) hide show
  1. package/dist/agents/skills-status.js +23 -3
  2. package/dist/agents/skills.js +1 -0
  3. package/dist/agents/system-prompt.js +3 -0
  4. package/dist/agents/taskmaster-tools.js +5 -0
  5. package/dist/agents/tool-policy.js +2 -1
  6. package/dist/agents/tools/authorize-admin-tool.js +1 -1
  7. package/dist/agents/tools/memory-tool.js +2 -1
  8. package/dist/agents/tools/software-update-tool.js +114 -0
  9. package/dist/auto-reply/reply/commands-status.js +5 -9
  10. package/dist/auto-reply/reply/get-reply-run.js +1 -1
  11. package/dist/auto-reply/reply/get-reply.js +1 -1
  12. package/dist/auto-reply/reply/model-selection.js +1 -1
  13. package/dist/browser/routes/screencast.js +1 -1
  14. package/dist/browser/screencast.js +1 -1
  15. package/dist/build-info.json +3 -3
  16. package/dist/commands/agent.js +2 -2
  17. package/dist/config/zod-schema.js +12 -1
  18. package/dist/control-ui/assets/index-B2FEGOCu.css +1 -0
  19. package/dist/control-ui/assets/index-nLVF-pVT.js +3762 -0
  20. package/dist/control-ui/assets/index-nLVF-pVT.js.map +1 -0
  21. package/dist/control-ui/index.html +2 -2
  22. package/dist/control-ui/maxy-icon.png +0 -0
  23. package/dist/cron/isolated-agent/recipients.js +70 -0
  24. package/dist/cron/isolated-agent/run.js +43 -13
  25. package/dist/gateway/config-reload.js +1 -0
  26. package/dist/gateway/control-ui.js +111 -5
  27. package/dist/gateway/protocol/index.js +6 -1
  28. package/dist/gateway/protocol/schema/agents-models-skills.js +23 -0
  29. package/dist/gateway/protocol/schema/protocol-schemas.js +6 -1
  30. package/dist/gateway/server-http.js +6 -1
  31. package/dist/gateway/server-methods/access.js +3 -3
  32. package/dist/gateway/server-methods/brand.js +160 -0
  33. package/dist/gateway/server-methods/browser-screencast.js +3 -3
  34. package/dist/gateway/server-methods/skills.js +159 -3
  35. package/dist/gateway/server-methods/workspaces.js +7 -7
  36. package/dist/gateway/server-methods-list.js +5 -0
  37. package/dist/gateway/server-methods.js +2 -0
  38. package/dist/gateway/server.impl.js +1 -1
  39. package/dist/infra/heartbeat-runner.js +17 -0
  40. package/dist/infra/heartbeat-update-notify.js +120 -0
  41. package/dist/infra/tunnel.js +1 -1
  42. package/dist/memory/embeddings.js +0 -4
  43. package/dist/memory/manager.js +15 -6
  44. package/dist/web/inbound/media.js +1 -1
  45. package/dist/web/login-qr.js +0 -23
  46. package/dist/web/providers/cloud/receive.js +1 -1
  47. package/dist/web/providers/cloud/webhook.js +1 -1
  48. package/package.json +1 -1
  49. package/skills/skill-builder/SKILL.md +97 -0
  50. package/skills/skill-builder/references/lean-pattern.md +118 -0
  51. package/skills/zero-to-prototype/SKILL.md +35 -0
  52. package/skills/zero-to-prototype/references/discovery.md +64 -0
  53. package/skills/zero-to-prototype/references/prd.md +83 -0
  54. package/skills/zero-to-prototype/references/validation.md +67 -0
  55. package/taskmaster-docs/USER-GUIDE.md +65 -31
  56. package/templates/customer/agents/public/AGENTS.md +3 -10
  57. package/templates/taskmaster/agents/public/SOUL.md +0 -4
  58. package/templates/tradesupport/agents/public/AGENTS.md +3 -10
  59. package/dist/control-ui/assets/index-DjhCZlZd.css +0 -1
  60. package/dist/control-ui/assets/index-DtuDNTAC.js +0 -3539
  61. package/dist/control-ui/assets/index-DtuDNTAC.js.map +0 -1
  62. package/skills/taskmaster/SKILL.md +0 -164
@@ -1,6 +1,7 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
2
3
  import { CONFIG_DIR } from "../utils.js";
3
- import { hasBinary, isBundledSkillAllowed, isConfigPathTruthy, loadWorkspaceSkillEntries, resolveBundledAllowlist, resolveConfigPath, resolveSkillConfig, resolveSkillsInstallPreferences, } from "./skills.js";
4
+ import { hasBinary, isBundledSkillAllowed, isConfigPathTruthy, loadWorkspaceSkillEntries, resolveBundledAllowlist, resolveBundledSkillsDir, resolveConfigPath, resolveSkillConfig, resolveSkillsInstallPreferences, } from "./skills.js";
4
5
  function resolveSkillKey(entry) {
5
6
  return entry.taskmaster?.skillKey ?? entry.skill.name;
6
7
  }
@@ -76,7 +77,7 @@ function normalizeInstallOptions(entry, prefs) {
76
77
  return [];
77
78
  return [toOption(preferred.spec, preferred.index)];
78
79
  }
79
- function buildSkillStatus(entry, config, prefs, eligibility) {
80
+ function buildSkillStatus(entry, config, prefs, eligibility, bundledSkillNames) {
80
81
  const skillKey = resolveSkillKey(entry);
81
82
  const skillConfig = resolveSkillConfig(config, skillKey);
82
83
  const disabled = skillConfig?.enabled === false;
@@ -145,6 +146,9 @@ function buildSkillStatus(entry, config, prefs, eligibility) {
145
146
  missing.env.length === 0 &&
146
147
  missing.config.length === 0 &&
147
148
  missing.os.length === 0));
149
+ const preloaded = bundledSkillNames
150
+ ? bundledSkillNames.has(entry.skill.name)
151
+ : false;
148
152
  return {
149
153
  name: entry.skill.name,
150
154
  description: entry.skill.description,
@@ -159,6 +163,7 @@ function buildSkillStatus(entry, config, prefs, eligibility) {
159
163
  disabled,
160
164
  blockedByAllowlist,
161
165
  eligible,
166
+ preloaded,
162
167
  requirements: {
163
168
  bins: requiredBins,
164
169
  anyBins: requiredAnyBins,
@@ -171,13 +176,28 @@ function buildSkillStatus(entry, config, prefs, eligibility) {
171
176
  install: normalizeInstallOptions(entry, prefs ?? resolveSkillsInstallPreferences(config)),
172
177
  };
173
178
  }
179
+ function resolveBundledSkillNames() {
180
+ const dir = resolveBundledSkillsDir();
181
+ if (!dir)
182
+ return new Set();
183
+ try {
184
+ return new Set(fs
185
+ .readdirSync(dir, { withFileTypes: true })
186
+ .filter((d) => d.isDirectory())
187
+ .map((d) => d.name));
188
+ }
189
+ catch {
190
+ return new Set();
191
+ }
192
+ }
174
193
  export function buildWorkspaceSkillStatus(workspaceDir, opts) {
175
194
  const managedSkillsDir = opts?.managedSkillsDir ?? path.join(CONFIG_DIR, "skills");
176
195
  const skillEntries = opts?.entries ?? loadWorkspaceSkillEntries(workspaceDir, opts);
177
196
  const prefs = resolveSkillsInstallPreferences(opts?.config);
197
+ const bundledSkillNames = resolveBundledSkillNames();
178
198
  return {
179
199
  workspaceDir,
180
200
  managedSkillsDir,
181
- skills: skillEntries.map((entry) => buildSkillStatus(entry, opts?.config, prefs, opts?.eligibility)),
201
+ skills: skillEntries.map((entry) => buildSkillStatus(entry, opts?.config, prefs, opts?.eligibility, bundledSkillNames)),
182
202
  };
183
203
  }
@@ -1,5 +1,6 @@
1
1
  export { hasBinary, isBundledSkillAllowed, isConfigPathTruthy, resolveBundledAllowlist, resolveConfigPath, resolveRuntimePlatform, resolveSkillConfig, } from "./skills/config.js";
2
2
  export { applySkillEnvOverrides, applySkillEnvOverridesFromSnapshot, } from "./skills/env-overrides.js";
3
+ export { resolveBundledSkillsDir } from "./skills/bundled-dir.js";
3
4
  export { buildWorkspaceSkillSnapshot, buildWorkspaceSkillsPrompt, buildWorkspaceSkillCommandSpecs, filterWorkspaceSkillEntries, loadWorkspaceSkillEntries, resolveSkillsPromptForRun, syncSkillsToWorkspace, } from "./skills/workspace.js";
4
5
  export function resolveSkillsInstallPreferences(config) {
5
6
  const raw = config?.skills?.install;
@@ -91,6 +91,7 @@ function buildMessagingSection(params) {
91
91
  "- For `action=send`, include `to` and `message`.",
92
92
  `- If multiple channels are configured, pass \`channel\` (${params.messageChannelOptions}).`,
93
93
  `- If you use \`message\` (\`action=send\`) to deliver your user-visible reply, respond with ONLY: ${SILENT_REPLY_TOKEN} (avoid duplicate replies).`,
94
+ "- CRITICAL: Your text output stays in this session — it does NOT reach end users on WhatsApp, iMessage, or other channels. The `message` tool is the ONLY way to deliver a message to someone outside this session. Never claim you sent a message unless the `message` tool returned a confirmed result.",
94
95
  params.inlineButtonsEnabled
95
96
  ? "- Inline buttons supported. Use `action=send` with `buttons=[[{text,callback_data}]]` (callback_data routes back as a user message)."
96
97
  : params.runtimeChannel
@@ -148,6 +149,7 @@ export function buildAgentSystemPrompt(params) {
148
149
  cron: "Manage scheduled events and wake events (use for reminders; when scheduling a reminder, write the systemEvent text as something that will read like a reminder when it fires, and mention that it is a reminder depending on the time gap between setting and firing; include recent context in reminder text if appropriate)",
149
150
  message: "Send messages and channel actions",
150
151
  gateway: "Restart, apply config, or run updates on the running Taskmaster process",
152
+ software_update: "Check for software updates, install them, or restart the gateway",
151
153
  agents_list: "List agent ids allowed for sessions_spawn",
152
154
  sessions_list: "List other sessions (incl. sub-agents) with filters/last",
153
155
  sessions_history: "Fetch history for another session/sub-agent",
@@ -176,6 +178,7 @@ export function buildAgentSystemPrompt(params) {
176
178
  "cron",
177
179
  "message",
178
180
  "gateway",
181
+ "software_update",
179
182
  "agents_list",
180
183
  "sessions_list",
181
184
  "sessions_history",
@@ -28,6 +28,7 @@ import { createRelayMessageTool } from "./tools/relay-message-tool.js";
28
28
  import { createSkillReadTool } from "./tools/skill-read-tool.js";
29
29
  import { createApiKeysTool } from "./tools/apikeys-tool.js";
30
30
  import { createImageGenerateTool } from "./tools/image-generate-tool.js";
31
+ import { createSoftwareUpdateTool } from "./tools/software-update-tool.js";
31
32
  export function createTaskmasterTools(options) {
32
33
  const imageTool = options?.agentDir?.trim()
33
34
  ? createImageTool({
@@ -86,6 +87,10 @@ export function createTaskmasterTools(options) {
86
87
  agentSessionKey: options?.agentSessionKey,
87
88
  config: options?.config,
88
89
  }),
90
+ createSoftwareUpdateTool({
91
+ agentSessionKey: options?.agentSessionKey,
92
+ config: options?.config,
93
+ }),
89
94
  createAgentsListTool({
90
95
  agentSessionKey: options?.agentSessionKey,
91
96
  requesterAgentIdOverride: options?.requesterAgentIdOverride,
@@ -31,7 +31,7 @@ export const TOOL_GROUPS = {
31
31
  // UI helpers
32
32
  "group:ui": ["browser", "canvas"],
33
33
  // Automation + infra
34
- "group:automation": ["cron", "gateway", "api_keys"],
34
+ "group:automation": ["cron", "gateway", "api_keys", "software_update"],
35
35
  // Messaging surface
36
36
  "group:messaging": ["message"],
37
37
  // Nodes + device tools
@@ -61,6 +61,7 @@ export const TOOL_GROUPS = {
61
61
  "web_fetch",
62
62
  "image",
63
63
  "image_generate",
64
+ "software_update",
64
65
  "current_time",
65
66
  "authorize_admin",
66
67
  "revoke_admin",
@@ -7,7 +7,7 @@ import { jsonResult } from "./common.js";
7
7
  * Strips spaces, dashes, parens. Ensures leading +.
8
8
  */
9
9
  function normalizePhoneNumber(input) {
10
- const digits = input.replace(/[\s\-\(\)]/g, "");
10
+ const digits = input.replace(/[\s\-()]/g, "");
11
11
  if (digits.startsWith("+"))
12
12
  return digits;
13
13
  return `+${digits}`;
@@ -144,7 +144,8 @@ export function createMemoryWriteTool(options) {
144
144
  label: "Memory Write",
145
145
  name: "memory_write",
146
146
  description: "Write or append content to a file in the memory/ directory. " +
147
- "Path must be within the session's allowed scope (e.g., memory/users/{phone}/). " +
147
+ "Path must be within the session's allowed scope. " +
148
+ "Phone numbers in paths MUST include the + prefix (e.g., memory/users/+447734875155/profile.md). " +
148
149
  "Creates parent directories if needed. Use mode='append' to add to existing content.",
149
150
  parameters: MemoryWriteSchema,
150
151
  execute: async (_toolCallId, params) => {
@@ -0,0 +1,114 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { loadConfig } from "../../config/io.js";
3
+ import { resolveStorePath, loadSessionStore } from "../../config/sessions.js";
4
+ import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js";
5
+ import { formatDoctorNonInteractiveHint, writeRestartSentinel, } from "../../infra/restart-sentinel.js";
6
+ import { stringEnum } from "../schema/typebox.js";
7
+ import { jsonResult, readStringParam } from "./common.js";
8
+ import { callGatewayTool } from "./gateway.js";
9
+ const SOFTWARE_UPDATE_ACTIONS = ["check", "run", "restart"];
10
+ const SoftwareUpdateToolSchema = Type.Object({
11
+ action: stringEnum(SOFTWARE_UPDATE_ACTIONS),
12
+ // restart
13
+ reason: Type.Optional(Type.String()),
14
+ // run
15
+ note: Type.Optional(Type.String()),
16
+ sessionKey: Type.Optional(Type.String()),
17
+ });
18
+ /**
19
+ * Narrowly scoped tool for software updates and gateway restarts.
20
+ * Unlike the full `gateway` tool, this exposes NO config mutation —
21
+ * only version checking, update execution, and restart.
22
+ */
23
+ export function createSoftwareUpdateTool(opts) {
24
+ return {
25
+ label: "Software Update",
26
+ name: "software_update",
27
+ description: "Check for software updates, install them, or restart the gateway. " +
28
+ "Use 'check' to see if an update is available, 'run' to install it, " +
29
+ "or 'restart' to restart the gateway process.",
30
+ parameters: SoftwareUpdateToolSchema,
31
+ execute: async (_toolCallId, args) => {
32
+ const params = args;
33
+ const action = readStringParam(params, "action", { required: true });
34
+ if (action === "check") {
35
+ const result = await callGatewayTool("update.status", {}, {});
36
+ return jsonResult({ ok: true, result });
37
+ }
38
+ if (action === "run") {
39
+ const sessionKey = typeof params.sessionKey === "string" && params.sessionKey.trim()
40
+ ? params.sessionKey.trim()
41
+ : opts?.agentSessionKey?.trim() || undefined;
42
+ const note = typeof params.note === "string" && params.note.trim() ? params.note.trim() : undefined;
43
+ const result = await callGatewayTool("update.run", {}, {
44
+ sessionKey,
45
+ note,
46
+ });
47
+ return jsonResult({ ok: true, result });
48
+ }
49
+ if (action === "restart") {
50
+ if (opts?.config?.commands?.restart !== true) {
51
+ throw new Error("Gateway restart is disabled. Set commands.restart=true to enable.");
52
+ }
53
+ const sessionKey = typeof params.sessionKey === "string" && params.sessionKey.trim()
54
+ ? params.sessionKey.trim()
55
+ : opts?.agentSessionKey?.trim() || undefined;
56
+ const reason = typeof params.reason === "string" && params.reason.trim()
57
+ ? params.reason.trim().slice(0, 200)
58
+ : undefined;
59
+ // Resolve delivery context for routing after restart
60
+ let deliveryContext;
61
+ let threadId;
62
+ if (sessionKey) {
63
+ const threadMarker = ":thread:";
64
+ const threadIndex = sessionKey.lastIndexOf(threadMarker);
65
+ const baseSessionKey = threadIndex === -1 ? sessionKey : sessionKey.slice(0, threadIndex);
66
+ const threadIdRaw = threadIndex === -1 ? undefined : sessionKey.slice(threadIndex + threadMarker.length);
67
+ threadId = threadIdRaw?.trim() || undefined;
68
+ try {
69
+ const cfg = loadConfig();
70
+ const storePath = resolveStorePath(cfg.session?.store);
71
+ const store = loadSessionStore(storePath);
72
+ let entry = store[sessionKey];
73
+ if (!entry?.deliveryContext && threadIndex !== -1 && baseSessionKey) {
74
+ entry = store[baseSessionKey];
75
+ }
76
+ if (entry?.deliveryContext) {
77
+ deliveryContext = {
78
+ channel: entry.deliveryContext.channel,
79
+ to: entry.deliveryContext.to,
80
+ accountId: entry.deliveryContext.accountId,
81
+ };
82
+ }
83
+ }
84
+ catch {
85
+ // best-effort
86
+ }
87
+ }
88
+ const payload = {
89
+ kind: "restart",
90
+ status: "ok",
91
+ ts: Date.now(),
92
+ sessionKey,
93
+ deliveryContext,
94
+ threadId,
95
+ message: reason ?? null,
96
+ doctorHint: formatDoctorNonInteractiveHint(),
97
+ stats: {
98
+ mode: "gateway.restart",
99
+ reason,
100
+ },
101
+ };
102
+ try {
103
+ await writeRestartSentinel(payload);
104
+ }
105
+ catch {
106
+ // sentinel is best-effort
107
+ }
108
+ const scheduled = scheduleGatewaySigusr1Restart({ reason });
109
+ return jsonResult(scheduled);
110
+ }
111
+ throw new Error(`Unknown action: ${action}`);
112
+ },
113
+ };
114
+ }
@@ -7,7 +7,6 @@ import { getCustomProviderApiKey, resolveEnvApiKey } from "../../agents/model-au
7
7
  import { resolveDefaultModelForAgent } from "../../agents/model-selection.js";
8
8
  import { resolveInternalSessionKey, resolveMainSessionAlias, } from "../../agents/tools/sessions-helpers.js";
9
9
  import { normalizeProviderId } from "../../agents/model-selection.js";
10
- import { loadConfig } from "../../config/config.js";
11
10
  import { logVerbose } from "../../globals.js";
12
11
  import { formatUsageWindowSummary, loadProviderUsageSummary, resolveUsageProviderId, } from "../../infra/provider-usage.js";
13
12
  import { normalizeGroupActivation } from "../group-activation.js";
@@ -74,21 +73,18 @@ export async function buildStatusReply(params) {
74
73
  logVerbose(`Ignoring /status from unauthorized sender: ${command.senderId || "<unknown>"}`);
75
74
  return undefined;
76
75
  }
77
- // Resolve model directly from config — fresh read, not from the passed-in cfg.
78
- // Config is the single source of truth. No passed objects, no cache.
79
- const freshCfg = loadConfig();
80
76
  const statusAgentId = sessionKey
81
- ? resolveSessionAgentId({ sessionKey, config: freshCfg })
82
- : resolveDefaultAgentId(freshCfg);
83
- const configModel = resolveDefaultModelForAgent({ cfg: freshCfg, agentId: statusAgentId });
77
+ ? resolveSessionAgentId({ sessionKey, config: cfg })
78
+ : resolveDefaultAgentId(cfg);
79
+ const configModel = resolveDefaultModelForAgent({ cfg, agentId: statusAgentId });
84
80
  const provider = configModel.provider;
85
81
  const model = configModel.model;
86
- const contextTokens = freshCfg.agents?.defaults?.contextTokens ??
82
+ const contextTokens = cfg.agents?.defaults?.contextTokens ??
87
83
  lookupContextTokens(model) ??
88
84
  DEFAULT_CONTEXT_TOKENS;
89
85
  // Diagnostic: log what status is resolving so we can trace mismatches
90
86
  console.log(`[status-resolve] agentId=${statusAgentId} sessionKey=${sessionKey} model=${provider}/${model}` +
91
- ` agentList=${(freshCfg.agents?.list ?? []).map((a) => `${a.id}:${a.model ?? "default"}`).join(",")}`);
87
+ ` agentList=${(cfg.agents?.list ?? []).map((a) => `${a.id}:${typeof a.model === "string" ? a.model : (a.model?.primary ?? "default")}`).join(",")}`);
92
88
  const statusAgentDir = resolveAgentDir(cfg, statusAgentId);
93
89
  const currentUsageProvider = (() => {
94
90
  try {
@@ -23,7 +23,7 @@ import { ensureSkillSnapshot, prependSystemEvents } from "./session-updates.js";
23
23
  import { resolveTypingMode } from "./typing-mode.js";
24
24
  const BARE_SESSION_RESET_PROMPT = "A new session was started via /new or /reset. Say hi briefly (1-2 sentences) and ask what the user wants to do next. If the runtime model differs from default_model in the system prompt, mention the default model in the greeting. Do not mention internal steps, files, tools, or reasoning.";
25
25
  export async function runPreparedReply(params) {
26
- const { ctx, sessionCtx, cfg, agentId, agentDir, agentCfg, sessionCfg, commandAuthorized, command, commandSource, allowTextCommands, directives, defaultActivation, elevatedEnabled, elevatedAllowed, blockStreamingEnabled, blockReplyChunking, resolvedBlockStreamingBreak, modelState, perMessageQueueMode, perMessageQueueOptions, typing, opts, defaultProvider, defaultModel, timeoutMs, isNewSession, resetTriggered, systemSent, sessionKey, sessionId, storePath, workspaceDir, sessionStore, } = params;
26
+ const { ctx, sessionCtx, cfg, agentId: _agentId, agentDir, agentCfg, sessionCfg, commandAuthorized, command, commandSource, allowTextCommands, directives, defaultActivation, elevatedEnabled, elevatedAllowed, blockStreamingEnabled, blockReplyChunking, resolvedBlockStreamingBreak, modelState, perMessageQueueMode, perMessageQueueOptions, typing, opts, defaultProvider, defaultModel, timeoutMs, isNewSession, resetTriggered, systemSent, sessionKey, sessionId, storePath, workspaceDir, sessionStore, } = params;
27
27
  // Model comes from config. Two params: config + agentId. Read fresh every time.
28
28
  const llmCfg = loadConfig();
29
29
  const llmAgentId = resolveSessionAgentId({ sessionKey, config: llmCfg });
@@ -36,7 +36,7 @@ export async function getReplyFromConfig(ctx, opts, configOverride) {
36
36
  let model = defaultModel;
37
37
  // Diagnostic: log agent/model resolution for every message so we can trace mismatches
38
38
  console.log(`[model-resolve] agentId=${agentId} sessionKey=${agentSessionKey} model=${provider}/${model}` +
39
- ` agentList=${(cfg.agents?.list ?? []).map((a) => `${a.id}:${a.model ?? "default"}`).join(",")}`);
39
+ ` agentList=${(cfg.agents?.list ?? []).map((a) => `${a.id}:${typeof a.model === "string" ? a.model : (a.model?.primary ?? "default")}`).join(",")}`);
40
40
  if (opts?.isHeartbeat) {
41
41
  const heartbeatRaw = agentCfg?.heartbeat?.model?.trim() ?? "";
42
42
  const heartbeatRef = heartbeatRaw
@@ -124,7 +124,7 @@ function scoreFuzzyMatch(params) {
124
124
  };
125
125
  }
126
126
  export async function createModelSelectionState(params) {
127
- const { cfg, agentCfg, sessionEntry, sessionStore, sessionKey, parentSessionKey, storePath, defaultProvider, defaultModel, } = params;
127
+ const { cfg, agentCfg, sessionEntry, sessionStore, sessionKey, parentSessionKey: _parentSessionKey, storePath, defaultProvider, defaultModel, } = params;
128
128
  // Model always comes from config (per-agent or global default).
129
129
  // No session-level model overrides — the config is the single source of truth,
130
130
  // applied to all conversations for the agent from that point forward.
@@ -99,7 +99,7 @@ export function registerBrowserScreencastRoutes(app, ctx) {
99
99
  res.json({ ok: true, sessionId: session.id, label });
100
100
  }
101
101
  catch (err) {
102
- log.error(`screencast start failed: ${err}`);
102
+ log.error(`screencast start failed: ${String(err)}`);
103
103
  jsonError(res, 500, String(err));
104
104
  }
105
105
  });
@@ -108,7 +108,7 @@ export async function startScreencast(opts) {
108
108
  send("Page.stopScreencast").catch(() => { });
109
109
  pollInterval = setInterval(pollOnce, POLL_INTERVAL_MS);
110
110
  // Fire one immediately so the UI gets a frame right away
111
- pollOnce();
111
+ void pollOnce();
112
112
  };
113
113
  ws.on("message", (raw) => {
114
114
  try {
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.106",
3
- "commit": "6c7e43fa0df93753260cac243239df6d5d8fc554",
4
- "builtAt": "2026-02-23T08:24:09.270Z"
2
+ "version": "1.0.108",
3
+ "commit": "efdfa6024c29f8a32d86ed0fd3b3b7a56410e868",
4
+ "builtAt": "2026-02-23T14:27:19.021Z"
5
5
  }
@@ -191,7 +191,7 @@ export async function agentCommand(opts, runtime = defaultRuntime, deps = create
191
191
  // Model always comes from config (per-agent or global default).
192
192
  // No session-level model overrides.
193
193
  const hasAllowlist = !!(agentCfg?.models && Object.keys(agentCfg.models).length > 0);
194
- let allowedModelKeys = new Set();
194
+ let _allowedModelKeys = new Set();
195
195
  let allowedModelCatalog = [];
196
196
  let modelCatalog = null;
197
197
  if (hasAllowlist) {
@@ -202,7 +202,7 @@ export async function agentCommand(opts, runtime = defaultRuntime, deps = create
202
202
  defaultProvider,
203
203
  defaultModel,
204
204
  });
205
- allowedModelKeys = allowed.allowedKeys;
205
+ _allowedModelKeys = allowed.allowedKeys;
206
206
  allowedModelCatalog = allowed.allowedCatalog;
207
207
  }
208
208
  if (sessionEntry) {
@@ -538,7 +538,18 @@ export const TaskmasterSchema = z
538
538
  .strict()
539
539
  .optional(),
540
540
  workspaces: z
541
- .record(z.string(), z.object({ displayName: z.string().optional() }).strict())
541
+ .record(z.string(), z
542
+ .object({
543
+ displayName: z.string().optional(),
544
+ brand: z
545
+ .object({
546
+ accentColor: HexColorSchema.optional(),
547
+ backgroundColor: HexColorSchema.optional(),
548
+ })
549
+ .strict()
550
+ .optional(),
551
+ })
552
+ .strict())
542
553
  .optional(),
543
554
  apiKeys: z.record(z.string(), z.string()).optional(),
544
555
  access: z