@rubytech/taskmaster 1.12.1 → 1.12.3

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 (32) hide show
  1. package/dist/agents/taskmaster-tools.js +5 -3
  2. package/dist/agents/tool-policy.js +1 -1
  3. package/dist/agents/tools/authorize-admin-tool.js +58 -17
  4. package/dist/agents/tools/bootstrap-tool.js +51 -0
  5. package/dist/agents/tools/cron-tool.js +20 -7
  6. package/dist/build-info.json +3 -3
  7. package/dist/commands/agents.config.js +1 -0
  8. package/dist/control-ui/assets/{index-4h8fLLNN.js → index-CP9IoaZp.js} +81 -70
  9. package/dist/control-ui/assets/index-CP9IoaZp.js.map +1 -0
  10. package/dist/control-ui/index.html +1 -1
  11. package/dist/gateway/protocol/schema/cron.js +8 -0
  12. package/dist/gateway/server-channels.js +6 -2
  13. package/dist/gateway/server-methods/cron.js +32 -0
  14. package/dist/gateway/server-methods/tailscale.js +9 -0
  15. package/dist/gateway/server-methods/workspaces.js +52 -0
  16. package/dist/web/auto-reply/monitor.js +3 -0
  17. package/package.json +1 -1
  18. package/skills/tailscale/SKILL.md +37 -9
  19. package/templates/beagle-taxi/agents/admin/AGENTS.md +25 -0
  20. package/templates/beagle-taxi/agents/admin/IDENTITY.md +9 -0
  21. package/templates/beagle-taxi/agents/admin/SOUL.md +31 -0
  22. package/templates/beagle-taxi/agents/public/AGENTS.md +54 -0
  23. package/templates/beagle-taxi/agents/public/IDENTITY.md +10 -0
  24. package/templates/beagle-taxi/agents/public/SOUL.md +32 -0
  25. package/templates/beagle-taxi/memory/public/knowledge-base.md +177 -0
  26. package/templates/beagle-taxi/skills/beagle-taxi/SKILL.md +44 -0
  27. package/templates/customer/agents/admin/BOOTSTRAP.md +2 -2
  28. package/templates/education-hero/agents/admin/BOOTSTRAP.md +2 -2
  29. package/templates/maxy/agents/admin/BOOTSTRAP.md +2 -3
  30. package/templates/taskmaster/agents/admin/BOOTSTRAP.md +2 -2
  31. package/templates/tradesupport/agents/admin/BOOTSTRAP.md +2 -2
  32. package/dist/control-ui/assets/index-4h8fLLNN.js.map +0 -1
@@ -21,6 +21,7 @@ import { createWebFetchTool, createWebSearchTool } from "./tools/web-tools.js";
21
21
  import { createTtsTool } from "./tools/tts-tool.js";
22
22
  import { createCurrentTimeTool } from "./tools/current-time.js";
23
23
  import { createAuthorizeAdminTool, createListAdminsTool, createRevokeAdminTool, } from "./tools/authorize-admin-tool.js";
24
+ import { createBootstrapCompleteTool } from "./tools/bootstrap-tool.js";
24
25
  import { createLicenseGenerateTool } from "./tools/license-tool.js";
25
26
  import { createContactCreateTool } from "./tools/contact-create-tool.js";
26
27
  import { createContactDeleteTool } from "./tools/contact-delete-tool.js";
@@ -147,9 +148,10 @@ export function createTaskmasterTools(options) {
147
148
  sandboxRoot: options?.sandboxRoot,
148
149
  }),
149
150
  createCurrentTimeTool({ config: options?.config }),
150
- createAuthorizeAdminTool(),
151
- createRevokeAdminTool(),
152
- createListAdminsTool(),
151
+ createAuthorizeAdminTool({ agentAccountId: options?.agentAccountId }),
152
+ createRevokeAdminTool({ agentAccountId: options?.agentAccountId }),
153
+ createListAdminsTool({ agentAccountId: options?.agentAccountId }),
154
+ createBootstrapCompleteTool({ agentSessionKey: options?.agentSessionKey }),
153
155
  createLicenseGenerateTool(),
154
156
  createContactCreateTool(),
155
157
  createContactDeleteTool(),
@@ -45,7 +45,7 @@ export const TOOL_GROUPS = {
45
45
  "verify_contact_code",
46
46
  ],
47
47
  // Admin management tools
48
- "group:admin": ["authorize_admin", "revoke_admin", "list_admins"],
48
+ "group:admin": ["authorize_admin", "revoke_admin", "list_admins", "bootstrap_complete"],
49
49
  // Control panel tools — expose setup/dashboard functionality to agents
50
50
  "group:control-panel": [
51
51
  "system_status",
@@ -13,9 +13,30 @@ function normalizePhoneNumber(input) {
13
13
  return `+${digits}`;
14
14
  }
15
15
  /**
16
- * Check if a binding already exists for this peer.
16
+ * Resolve the admin agent ID for a given account.
17
+ * Looks up the existing paired-admin binding for the account, which is the
18
+ * authoritative record of which agent handles admin DMs. Falls back to the
19
+ * naming convention "{accountId}-admin", then to "admin" when no account scope.
17
20
  */
18
- function findExistingBinding(bindings, channel, peerId) {
21
+ function resolveAdminAgentId(bindings, accountId) {
22
+ if (accountId) {
23
+ const paired = bindings.find((b) => b.match.channel === "whatsapp" &&
24
+ b.match.accountId === accountId &&
25
+ b.match.peer?.kind === "dm" &&
26
+ b.meta?.paired === true);
27
+ if (paired)
28
+ return paired.agentId;
29
+ return `${accountId}-admin`;
30
+ }
31
+ return "admin";
32
+ }
33
+ /**
34
+ * Check if a binding already exists for this peer, optionally scoped to an account.
35
+ * When accountId is provided, bindings for a different specific account are not
36
+ * considered conflicts — only bindings for the same account or cross-account
37
+ * wildcards (no accountId) are.
38
+ */
39
+ function findExistingBinding(bindings, channel, peerId, accountId) {
19
40
  const normalizedPeerId = peerId.toLowerCase();
20
41
  return bindings.find((b) => {
21
42
  if (b.match.channel !== channel)
@@ -23,7 +44,16 @@ function findExistingBinding(bindings, channel, peerId) {
23
44
  if (b.match.peer?.kind !== "dm")
24
45
  return false;
25
46
  const existingId = b.match.peer.id?.toLowerCase();
26
- return existingId === normalizedPeerId;
47
+ if (existingId !== normalizedPeerId)
48
+ return false;
49
+ // When an account scope is given, exclude bindings scoped to a different account.
50
+ // A binding with no accountId is a cross-account wildcard and always conflicts.
51
+ if (accountId !== undefined &&
52
+ b.match.accountId !== undefined &&
53
+ b.match.accountId !== accountId) {
54
+ return false;
55
+ }
56
+ return true;
27
57
  });
28
58
  }
29
59
  const AuthorizeAdminSchema = Type.Object({
@@ -37,7 +67,7 @@ const RevokeAdminSchema = Type.Object({
37
67
  }),
38
68
  });
39
69
  const ListAdminsSchema = Type.Object({});
40
- export function createAuthorizeAdminTool() {
70
+ export function createAuthorizeAdminTool(options) {
41
71
  return {
42
72
  label: "Admin",
43
73
  name: "authorize_admin",
@@ -45,7 +75,7 @@ export function createAuthorizeAdminTool() {
45
75
  parameters: AuthorizeAdminSchema,
46
76
  execute: async (_toolCallId, params) => {
47
77
  const phoneNumber = normalizePhoneNumber(params.phoneNumber);
48
- const agentId = "admin";
78
+ const accountId = options?.agentAccountId;
49
79
  // Validate phone number format
50
80
  if (!/^\+\d{7,15}$/.test(phoneNumber)) {
51
81
  return jsonResult({
@@ -63,8 +93,10 @@ export function createAuthorizeAdminTool() {
63
93
  }
64
94
  const parsedConfig = structuredClone(snapshot.parsed);
65
95
  const existingBindings = (parsedConfig.bindings ?? []);
66
- // Check if binding already exists
67
- const existing = findExistingBinding(existingBindings, "whatsapp", phoneNumber);
96
+ // Resolve the admin agent for this account from the existing paired binding
97
+ const agentId = resolveAdminAgentId(existingBindings, accountId);
98
+ // Check if binding already exists within this account scope
99
+ const existing = findExistingBinding(existingBindings, "whatsapp", phoneNumber, accountId);
68
100
  if (existing) {
69
101
  if (existing.agentId === agentId) {
70
102
  return jsonResult({
@@ -79,16 +111,19 @@ export function createAuthorizeAdminTool() {
79
111
  error: `${phoneNumber} is already bound to the ${existing.agentId} agent. Remove that binding first.`,
80
112
  });
81
113
  }
82
- // Create new binding
114
+ // Create new binding scoped to this agent and account
83
115
  const newBinding = {
84
116
  agentId,
85
117
  match: {
86
118
  channel: "whatsapp",
119
+ ...(accountId ? { accountId } : {}),
87
120
  peer: { kind: "dm", id: phoneNumber },
88
121
  },
89
122
  };
90
- // Insert before the catch-all binding (accountId: "*")
91
- const catchAllIndex = existingBindings.findIndex((b) => b.match.channel === "whatsapp" && b.match.accountId === "*");
123
+ // Insert before the catch-all binding for this account (channel binding with no peer)
124
+ const catchAllIndex = existingBindings.findIndex((b) => b.match.channel === "whatsapp" &&
125
+ !b.match.peer &&
126
+ (accountId ? b.match.accountId === accountId : b.match.accountId === "*"));
92
127
  let updatedBindings;
93
128
  if (catchAllIndex >= 0) {
94
129
  updatedBindings = [
@@ -111,7 +146,7 @@ export function createAuthorizeAdminTool() {
111
146
  });
112
147
  }
113
148
  await writeConfigFile(validated.config);
114
- logVerbose(`Authorized admin: ${phoneNumber} -> ${agentId}`);
149
+ logVerbose(`Authorized admin: ${phoneNumber} -> ${agentId}${accountId ? ` (account: ${accountId})` : ""}`);
115
150
  return jsonResult({
116
151
  success: true,
117
152
  message: `Authorized ${phoneNumber} as an admin. They now have full management access.`,
@@ -120,7 +155,7 @@ export function createAuthorizeAdminTool() {
120
155
  },
121
156
  };
122
157
  }
123
- export function createRevokeAdminTool() {
158
+ export function createRevokeAdminTool(options) {
124
159
  return {
125
160
  label: "Admin",
126
161
  name: "revoke_admin",
@@ -128,6 +163,7 @@ export function createRevokeAdminTool() {
128
163
  parameters: RevokeAdminSchema,
129
164
  execute: async (_toolCallId, params) => {
130
165
  const phoneNumber = normalizePhoneNumber(params.phoneNumber);
166
+ const accountId = options?.agentAccountId;
131
167
  const snapshot = await readConfigFileSnapshot();
132
168
  if (!snapshot.valid || !snapshot.parsed || typeof snapshot.parsed !== "object") {
133
169
  return jsonResult({
@@ -137,7 +173,7 @@ export function createRevokeAdminTool() {
137
173
  }
138
174
  const parsedConfig = structuredClone(snapshot.parsed);
139
175
  const existingBindings = (parsedConfig.bindings ?? []);
140
- const existing = findExistingBinding(existingBindings, "whatsapp", phoneNumber);
176
+ const existing = findExistingBinding(existingBindings, "whatsapp", phoneNumber, accountId);
141
177
  if (!existing) {
142
178
  return jsonResult({
143
179
  success: false,
@@ -155,7 +191,7 @@ export function createRevokeAdminTool() {
155
191
  });
156
192
  }
157
193
  await writeConfigFile(validated.config);
158
- logVerbose(`Revoked admin: ${phoneNumber}`);
194
+ logVerbose(`Revoked admin: ${phoneNumber}${accountId ? ` (account: ${accountId})` : ""}`);
159
195
  return jsonResult({
160
196
  success: true,
161
197
  message: `Revoked admin access for ${phoneNumber}.`,
@@ -164,7 +200,7 @@ export function createRevokeAdminTool() {
164
200
  },
165
201
  };
166
202
  }
167
- export function createListAdminsTool() {
203
+ export function createListAdminsTool(options) {
168
204
  return {
169
205
  label: "Admin",
170
206
  name: "list_admins",
@@ -173,15 +209,20 @@ export function createListAdminsTool() {
173
209
  execute: async (_toolCallId) => {
174
210
  const cfg = loadConfig();
175
211
  const bindings = cfg.bindings ?? [];
212
+ const accountId = options?.agentAccountId;
213
+ // Resolve the admin agent for this account so we only list admins scoped here
214
+ const agentId = resolveAdminAgentId(bindings, accountId);
176
215
  const admins = bindings
177
216
  .filter((b) => {
178
- // Accept both "admin" and legacy "management" agent IDs
179
- if (b.agentId !== "admin" && b.agentId !== "management")
217
+ if (b.agentId !== agentId)
180
218
  return false;
181
219
  if (b.match.channel !== "whatsapp")
182
220
  return false;
183
221
  if (b.match.peer?.kind !== "dm")
184
222
  return false;
223
+ // Exclude bindings scoped to a different account
224
+ if (accountId && b.match.accountId && b.match.accountId !== accountId)
225
+ return false;
185
226
  return Boolean(b.match.peer.id);
186
227
  })
187
228
  .map((b) => b.match.peer?.id)
@@ -0,0 +1,51 @@
1
+ import { promises as fs } from "fs";
2
+ import path from "path";
3
+ import { Type } from "@sinclair/typebox";
4
+ import { resolveAgentWorkspaceDir } from "../agent-scope.js";
5
+ import { loadConfig } from "../../config/config.js";
6
+ import { logVerbose } from "../../globals.js";
7
+ import { DEFAULT_BOOTSTRAP_FILENAME, BOOTSTRAP_DONE_SENTINEL } from "../workspace.js";
8
+ import { resolveSessionAgentId } from "../agent-scope.js";
9
+ import { jsonResult } from "./common.js";
10
+ const BootstrapCompleteSchema = Type.Object({});
11
+ /**
12
+ * Tool that marks agent bootstrap as complete.
13
+ * Writes the .bootstrap-done sentinel (suppresses future BOOTSTRAP.md injection)
14
+ * and removes BOOTSTRAP.md if it exists.
15
+ *
16
+ * This is a privileged action — agents cannot write to the agents/ directory via
17
+ * general file tools, so this dedicated tool provides the safe, auditable path.
18
+ */
19
+ export function createBootstrapCompleteTool(opts) {
20
+ return {
21
+ label: "Setup",
22
+ name: "bootstrap_complete",
23
+ description: "Mark the initial setup as complete. Writes the .bootstrap-done sentinel so the setup guide is never shown again, then removes BOOTSTRAP.md. Call this after completing all steps in BOOTSTRAP.md.",
24
+ parameters: BootstrapCompleteSchema,
25
+ execute: async (_toolCallId) => {
26
+ const cfg = loadConfig();
27
+ const agentId = opts?.agentSessionKey
28
+ ? resolveSessionAgentId({ sessionKey: opts.agentSessionKey, config: cfg })
29
+ : undefined;
30
+ if (!agentId) {
31
+ return jsonResult({ success: false, error: "Cannot resolve agent identity." });
32
+ }
33
+ const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
34
+ const sentinelPath = path.join(workspaceDir, BOOTSTRAP_DONE_SENTINEL);
35
+ const bootstrapPath = path.join(workspaceDir, DEFAULT_BOOTSTRAP_FILENAME);
36
+ // Write the sentinel — this suppresses BOOTSTRAP.md injection on all future runs.
37
+ await fs.writeFile(sentinelPath, "", "utf-8");
38
+ logVerbose(`bootstrap_complete: wrote sentinel ${sentinelPath}`);
39
+ // Remove BOOTSTRAP.md if present — no longer needed once the sentinel exists.
40
+ const removed = await fs
41
+ .unlink(bootstrapPath)
42
+ .then(() => true)
43
+ .catch(() => false);
44
+ logVerbose(`bootstrap_complete: BOOTSTRAP.md ${removed ? "removed" : "already absent"}`);
45
+ return jsonResult({
46
+ success: true,
47
+ message: "Setup complete. This onboarding guide will no longer appear.",
48
+ });
49
+ },
50
+ };
51
+ }
@@ -231,31 +231,44 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con
231
231
  throw new Error("patch required");
232
232
  }
233
233
  const patch = normalizeCronJobPatch(params.patch) ?? params.patch;
234
- return jsonResult(await callGatewayTool("cron.update", gatewayOpts, {
235
- id,
236
- patch,
237
- }));
234
+ const updatePayload = { id, patch };
235
+ const updateAccountId = opts?.agentAccountId;
236
+ if (updateAccountId)
237
+ updatePayload.accountId = updateAccountId;
238
+ return jsonResult(await callGatewayTool("cron.update", gatewayOpts, updatePayload));
238
239
  }
239
240
  case "remove": {
240
241
  const id = readStringParam(params, "jobId") ?? readStringParam(params, "id");
241
242
  if (!id) {
242
243
  throw new Error("jobId required (id accepted for backward compatibility)");
243
244
  }
244
- return jsonResult(await callGatewayTool("cron.remove", gatewayOpts, { id }));
245
+ const removePayload = { id };
246
+ const removeAccountId = opts?.agentAccountId;
247
+ if (removeAccountId)
248
+ removePayload.accountId = removeAccountId;
249
+ return jsonResult(await callGatewayTool("cron.remove", gatewayOpts, removePayload));
245
250
  }
246
251
  case "run": {
247
252
  const id = readStringParam(params, "jobId") ?? readStringParam(params, "id");
248
253
  if (!id) {
249
254
  throw new Error("jobId required (id accepted for backward compatibility)");
250
255
  }
251
- return jsonResult(await callGatewayTool("cron.run", gatewayOpts, { id }));
256
+ const runPayload = { id };
257
+ const runAccountId = opts?.agentAccountId;
258
+ if (runAccountId)
259
+ runPayload.accountId = runAccountId;
260
+ return jsonResult(await callGatewayTool("cron.run", gatewayOpts, runPayload));
252
261
  }
253
262
  case "runs": {
254
263
  const id = readStringParam(params, "jobId") ?? readStringParam(params, "id");
255
264
  if (!id) {
256
265
  throw new Error("jobId required (id accepted for backward compatibility)");
257
266
  }
258
- return jsonResult(await callGatewayTool("cron.runs", gatewayOpts, { id }));
267
+ const runsPayload = { id };
268
+ const runsAccountId = opts?.agentAccountId;
269
+ if (runsAccountId)
270
+ runsPayload.accountId = runsAccountId;
271
+ return jsonResult(await callGatewayTool("cron.runs", gatewayOpts, runsPayload));
259
272
  }
260
273
  case "wake": {
261
274
  const text = readStringParam(params, "text", { required: true });
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.12.1",
3
- "commit": "9b25ea5bacf5efb9a4ad6dadef692ee7a44e7915",
4
- "builtAt": "2026-03-02T10:53:03.436Z"
2
+ "version": "1.12.3",
3
+ "commit": "3cb091fd087b2e5768cafce7e369b258e4130f0c",
4
+ "builtAt": "2026-03-02T19:12:56.794Z"
5
5
  }
@@ -90,6 +90,7 @@ export function applyAgentConfig(cfg, params) {
90
90
  ...(params.workspace ? { workspace: params.workspace } : {}),
91
91
  ...(params.agentDir ? { agentDir: params.agentDir } : {}),
92
92
  ...(params.model ? { model: params.model } : {}),
93
+ ...(params.tools ? { tools: params.tools } : {}),
93
94
  };
94
95
  const nextList = [...list];
95
96
  if (index >= 0) {