@rubytech/taskmaster 1.0.90 → 1.0.91

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.
@@ -19,7 +19,9 @@ const applyPatchSchema = Type.Object({
19
19
  description: "Patch content using the *** Begin Patch/End Patch format.",
20
20
  }),
21
21
  });
22
- export function createApplyPatchTool(options = {}) {
22
+ export function createApplyPatchTool(options = {}
23
+ // biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-agent-core uses a different module instance.
24
+ ) {
23
25
  const cwd = options.cwd ?? process.cwd();
24
26
  const sandboxRoot = options.sandboxRoot;
25
27
  return {
@@ -513,7 +513,9 @@ async function runExecProcess(opts) {
513
513
  kill: () => killSession(session),
514
514
  };
515
515
  }
516
- export function createExecTool(defaults) {
516
+ export function createExecTool(defaults
517
+ // biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-agent-core uses a different module instance.
518
+ ) {
517
519
  const defaultBackgroundMs = clampNumber(defaults?.backgroundMs ?? readEnvInt("PI_BASH_YIELD_MS"), 10_000, 10, 120_000);
518
520
  const allowBackground = defaults?.allowBackground ?? true;
519
521
  const defaultTimeoutSec = typeof defaults?.timeoutSec === "number" && defaults.timeoutSec > 0
@@ -15,7 +15,9 @@ const processSchema = Type.Object({
15
15
  offset: Type.Optional(Type.Number({ description: "Log offset" })),
16
16
  limit: Type.Optional(Type.Number({ description: "Log length" })),
17
17
  });
18
- export function createProcessTool(defaults) {
18
+ export function createProcessTool(defaults
19
+ // biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-agent-core uses a different module instance.
20
+ ) {
19
21
  if (defaults?.cleanupMs !== undefined) {
20
22
  setJobTtlMs(defaults.cleanupMs);
21
23
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.90",
2
+ "version": "1.0.91",
3
3
  "commit": "d12721761c03ecc5631483c59e1696d0ddba08a3",
4
- "builtAt": "2026-02-21T00:10:58.623Z"
4
+ "builtAt": "2026-02-21T00:30:04.834Z"
5
5
  }
@@ -1,6 +1,6 @@
1
1
  import { loadConfig } from "../../config/config.js";
2
2
  import { resolveTaskmasterPackageRoot } from "../../infra/taskmaster-root.js";
3
- import { scheduleGatewaySigusr1Restart, triggerTaskmasterRestart } from "../../infra/restart.js";
3
+ import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js";
4
4
  import { formatDoctorNonInteractiveHint, readRestartSentinel, writeRestartSentinel, } from "../../infra/restart-sentinel.js";
5
5
  import { checkUpdateStatus, compareSemverStrings } from "../../infra/update-check.js";
6
6
  import { normalizeUpdateChannel, resolveEffectiveUpdateChannel, } from "../../infra/update-channels.js";
@@ -248,12 +248,13 @@ export const updateHandlers = {
248
248
  // Non-critical — unit file just won't have watchdog directives
249
249
  // until the next `daemon install --force`.
250
250
  }
251
- const attempt = triggerTaskmasterRestart();
252
- if (!attempt.ok) {
253
- log.error(`daemon restart failed: ${attempt.detail ?? "unknown"}`);
254
- // Fall back to in-process restart
255
- scheduleGatewaySigusr1Restart({ delayMs: 0, reason: "update.run (fallback)" });
256
- }
251
+ // Exit cleanly and let the supervisor (systemd Restart=always /
252
+ // launchd KeepAlive) restart us with the new code on disk.
253
+ // This avoids a race condition where spawnSync("systemctl restart")
254
+ // returns before our SIGTERM handler releases the port, causing the
255
+ // new process to fail with "port already in use" and crash-loop.
256
+ log.info("exiting for supervisor restart after global update");
257
+ process.exit(0);
257
258
  })();
258
259
  }, delayMs);
259
260
  restart = { ok: true };
@@ -5,28 +5,30 @@
5
5
  * ~/taskmaster/memory/ ← shared canonical location
6
6
  * public/ ← shared: both agents read/write
7
7
  * shared/ ← shared: both agents read/write
8
+ * users/ ← shared: both agents read/write (app-layer scoped)
9
+ * groups/ ← shared: both agents read/write (app-layer scoped)
8
10
  * ~/taskmaster/agents/admin/memory/ ← physical directory
9
11
  * admin/ ← physical: admin-only data
10
12
  * public → ../../../memory/public (symlink to shared)
11
13
  * shared → ../../../memory/shared (symlink to shared)
12
- * users/ physical: admin's per-user data
14
+ * users → ../../../memory/users (symlink to shared)
15
+ * groups → ../../../memory/groups (symlink to shared)
13
16
  * ~/taskmaster/agents/public/memory/ ← physical directory
14
17
  * public → ../../../memory/public (symlink to shared)
15
18
  * shared → ../../../memory/shared (symlink to shared)
16
- * users/ physical: public's per-user data
19
+ * users → ../../../memory/users (symlink to shared)
20
+ * groups → ../../../memory/groups (symlink to shared)
17
21
  *
18
- * Only `public/` and `shared/` are symlinked.
19
- * `admin/` only exists in the admin agent workspace.
20
- * `users/` is per-agent (each agent has its own user data).
22
+ * public/, shared/, users/, and groups/ are symlinked — both agents share
23
+ * the same data. Access control is enforced by the application layer (scope config).
24
+ * admin/ only exists in the admin agent workspace.
21
25
  */
22
26
  import fs from "node:fs/promises";
23
27
  import path from "node:path";
24
28
  import { createSubsystemLogger } from "../logging/subsystem.js";
25
29
  const log = createSubsystemLogger("memory-layout");
26
30
  /** Shared scope folders symlinked from each agent workspace. */
27
- const SHARED_FOLDERS = ["public", "shared"];
28
- /** Agent-specific folders that belong in per-agent memory, not at the shared root. */
29
- const AGENT_SPECIFIC_FOLDERS = ["admin", "users", "groups"];
31
+ const SHARED_FOLDERS = ["public", "shared", "users", "groups"];
30
32
  /** The relative symlink target from agents/{id}/memory/{scope} → ../../../memory/{scope} */
31
33
  const SYMLINK_TARGET_PREFIX = "../../../memory";
32
34
  async function exists(p) {
@@ -83,14 +85,14 @@ async function mergeDir(src, dest) {
83
85
  /**
84
86
  * Ensure the correct memory layout for a multi-agent workspace.
85
87
  *
86
- * - Creates the shared `memory/public` and `memory/shared` at workspace root
88
+ * - Creates the shared canonical directories at workspace root
87
89
  * - For each agent dir, ensures `memory/` is a physical directory with
88
- * symlinks for `public/` and `shared/`, and physical dirs for agent-specific data
90
+ * symlinks for shared folders and a physical `admin/` for the admin agent
89
91
  * - Migrates from the old full-symlink layout if found
90
- * - Consolidates data from broken physical dirs into the shared location
92
+ * - Consolidates data from physical dirs back into the shared location
91
93
  *
92
94
  * @param workspaceRoot The workspace root (e.g. ~/taskmaster)
93
- * @param agentDirs Agent workspace directories (e.g. [~/taskmaster/agents/admin, ~/taskmaster/agents/public])
95
+ * @param agentDirs Agent workspace directories
94
96
  */
95
97
  export async function ensureMemoryLayout(workspaceRoot, agentDirs) {
96
98
  const sharedMemoryDir = path.join(workspaceRoot, "memory");
@@ -104,11 +106,8 @@ export async function ensureMemoryLayout(workspaceRoot, agentDirs) {
104
106
  // Case 1: Full memory symlink (old layout) — migrate to per-subfolder
105
107
  if (await isSymlink(memoryPath)) {
106
108
  log.info(`migrating full memory symlink to per-subfolder layout: ${agentDir}`);
107
- // The symlink points to the shared memory dir — data is already there.
108
- // Remove the full symlink and create a physical dir with per-subfolder symlinks.
109
109
  await fs.unlink(memoryPath);
110
110
  await fs.mkdir(memoryPath, { recursive: true });
111
- // Data is already in the shared location, just create symlinks and physical dirs
112
111
  }
113
112
  // Ensure memory/ is a physical directory
114
113
  if (!(await isDirectory(memoryPath))) {
@@ -136,12 +135,10 @@ export async function ensureMemoryLayout(workspaceRoot, agentDirs) {
136
135
  /* ignore if race */
137
136
  });
138
137
  }
139
- // Ensure physical dirs for agent-specific data
138
+ // admin/ is the only per-agent physical directory — exists only in admin workspace
140
139
  if (isAdmin) {
141
140
  await fs.mkdir(path.join(memoryPath, "admin"), { recursive: true });
142
141
  }
143
- await fs.mkdir(path.join(memoryPath, "users"), { recursive: true });
144
- await fs.mkdir(path.join(memoryPath, "groups"), { recursive: true });
145
142
  // Remove admin/ from non-admin agent workspaces (it should never exist there)
146
143
  if (!isAdmin) {
147
144
  const adminPath = path.join(memoryPath, "admin");
@@ -151,7 +148,6 @@ export async function ensureMemoryLayout(workspaceRoot, agentDirs) {
151
148
  }
152
149
  else {
153
150
  log.warn(`non-admin agent has data in memory/admin/ — moving to admin agent`);
154
- // Find admin agent dir and merge there
155
151
  const adminAgent = agentDirs.find((a) => a.id.toLowerCase() === "admin" || a.id.toLowerCase().endsWith("-admin"));
156
152
  if (adminAgent) {
157
153
  const adminMemAdmin = path.join(adminAgent.dir, "memory", "admin");
@@ -163,57 +159,52 @@ export async function ensureMemoryLayout(workspaceRoot, agentDirs) {
163
159
  }
164
160
  }
165
161
  }
166
- // Sweep orphaned agent-specific data from the shared root.
167
- // After full-symlink migration, admin/, users/, groups/, notes/, and loose files
168
- // remain at ~/taskmaster/memory/ but are no longer accessible via per-subfolder symlinks.
169
- // Move them to the admin agent's workspace (most privileged, owns historical data).
162
+ // Sweep orphaned admin-only data from the shared root.
163
+ // After full-symlink migration, admin/, notes/, and loose files may remain
164
+ // at the shared root. These belong in the admin agent's workspace.
170
165
  await migrateOrphanedSharedData(sharedMemoryDir, agentDirs);
171
166
  }
172
167
  /**
173
- * Move agent-specific data that was left at the shared memory root
168
+ * Move admin-only data that was left at the shared memory root
174
169
  * (from the old full-symlink layout) into the admin agent's workspace.
175
170
  *
176
- * Only `public/` and `shared/` belong at the shared root.
177
- * Everything else is orphaned and needs to move to a per-agent location.
171
+ * Shared folders (public, shared, users, groups) belong at the shared root.
172
+ * Everything else (admin, notes, loose .md files) is admin-only.
178
173
  */
179
174
  async function migrateOrphanedSharedData(sharedMemoryDir, agentDirs) {
180
175
  const adminAgent = agentDirs.find((a) => a.id.toLowerCase() === "admin" || a.id.toLowerCase().endsWith("-admin"));
181
176
  if (!adminAgent)
182
- return; // No admin agent — nothing to migrate to
177
+ return;
183
178
  const adminMemoryPath = path.join(adminAgent.dir, "memory");
184
179
  let entries;
185
180
  try {
186
181
  entries = await fs.readdir(sharedMemoryDir);
187
182
  }
188
183
  catch {
189
- return; // Shared dir doesn't exist
184
+ return;
190
185
  }
191
186
  const sharedSet = new Set(SHARED_FOLDERS);
192
187
  for (const name of entries) {
193
188
  // Skip shared folders — they belong here
194
189
  if (sharedSet.has(name))
195
190
  continue;
196
- // Skip .DS_Store and other hidden files
191
+ // Skip hidden files
197
192
  if (name.startsWith("."))
198
193
  continue;
199
194
  const srcPath = path.join(sharedMemoryDir, name);
200
195
  if (await isDirectory(srcPath)) {
201
196
  const destPath = path.join(adminMemoryPath, name);
202
197
  if (await isEmptyDir(srcPath)) {
203
- // Empty orphan dir — just remove it
204
198
  await fs.rm(srcPath, { recursive: true }).catch(() => { });
205
199
  continue;
206
200
  }
207
- // Merge into admin agent's workspace
208
201
  log.info(`migrating orphaned ${name}/ from shared root to admin agent`);
209
202
  await mergeDir(srcPath, destPath);
210
- // Remove source if now empty
211
203
  if (await isEmptyDir(srcPath)) {
212
204
  await fs.rm(srcPath, { recursive: true }).catch(() => { });
213
205
  }
214
206
  }
215
207
  else if (name.endsWith(".md")) {
216
- // Loose .md files — move to admin agent's memory root
217
208
  const destPath = path.join(adminMemoryPath, name);
218
209
  if (!(await exists(destPath))) {
219
210
  log.info(`migrating orphaned ${name} from shared root to admin agent`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/taskmaster",
3
- "version": "1.0.90",
3
+ "version": "1.0.91",
4
4
  "description": "AI-powered business assistant for small businesses",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -105,6 +105,28 @@ if ! check_node; then
105
105
  install_node
106
106
  fi
107
107
 
108
+ # ── Stop existing daemon before upgrade ──────────────────────────────────────
109
+ # The old gateway process holds the port. It must be stopped before npm
110
+ # replaces the files on disk, otherwise the new daemon cannot bind.
111
+
112
+ if command -v taskmaster >/dev/null 2>&1; then
113
+ echo ""
114
+ echo "Stopping existing gateway daemon..."
115
+ if [ "$PLATFORM" = "linux" ] && [ "$(id -u)" = "0" ] && [ "${SUDO_USER:-}" != "" ]; then
116
+ # Running as root via sudo — must run as the real user for systemctl --user
117
+ _STOP_USER="${SUDO_USER}"
118
+ _STOP_UID=$(id -u "$_STOP_USER")
119
+ sudo -u "$_STOP_USER" \
120
+ XDG_RUNTIME_DIR="/run/user/$_STOP_UID" \
121
+ DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$_STOP_UID/bus" \
122
+ taskmaster gateway stop 2>/dev/null || true
123
+ else
124
+ taskmaster gateway stop 2>/dev/null || true
125
+ fi
126
+ # Give the process a moment to release the port
127
+ sleep 2
128
+ fi
129
+
108
130
  # ── Install Taskmaster via npm ───────────────────────────────────────────────
109
131
 
110
132
  echo ""