@rubytech/taskmaster 1.0.89 → 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.89",
2
+ "version": "1.0.91",
3
3
  "commit": "d12721761c03ecc5631483c59e1696d0ddba08a3",
4
- "builtAt": "2026-02-21T00:01:17.386Z"
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 };
@@ -34,6 +34,22 @@ async function walkDir(dir, files) {
34
34
  const entries = await fs.readdir(dir, { withFileTypes: true });
35
35
  for (const entry of entries) {
36
36
  const full = path.join(dir, entry.name);
37
+ // Symlinks need fs.stat to determine the target type
38
+ if (entry.isSymbolicLink()) {
39
+ try {
40
+ const stat = await fs.stat(full);
41
+ if (stat.isDirectory()) {
42
+ await walkDir(full, files);
43
+ }
44
+ else if (stat.isFile() && entry.name.endsWith(".md")) {
45
+ files.push(full);
46
+ }
47
+ }
48
+ catch {
49
+ // Broken symlink — skip
50
+ }
51
+ continue;
52
+ }
37
53
  if (entry.isDirectory()) {
38
54
  await walkDir(full, files);
39
55
  continue;
@@ -5,26 +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"];
31
+ const SHARED_FOLDERS = ["public", "shared", "users", "groups"];
28
32
  /** The relative symlink target from agents/{id}/memory/{scope} → ../../../memory/{scope} */
29
33
  const SYMLINK_TARGET_PREFIX = "../../../memory";
30
34
  async function exists(p) {
@@ -81,14 +85,14 @@ async function mergeDir(src, dest) {
81
85
  /**
82
86
  * Ensure the correct memory layout for a multi-agent workspace.
83
87
  *
84
- * - Creates the shared `memory/public` and `memory/shared` at workspace root
88
+ * - Creates the shared canonical directories at workspace root
85
89
  * - For each agent dir, ensures `memory/` is a physical directory with
86
- * 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
87
91
  * - Migrates from the old full-symlink layout if found
88
- * - Consolidates data from broken physical dirs into the shared location
92
+ * - Consolidates data from physical dirs back into the shared location
89
93
  *
90
94
  * @param workspaceRoot The workspace root (e.g. ~/taskmaster)
91
- * @param agentDirs Agent workspace directories (e.g. [~/taskmaster/agents/admin, ~/taskmaster/agents/public])
95
+ * @param agentDirs Agent workspace directories
92
96
  */
93
97
  export async function ensureMemoryLayout(workspaceRoot, agentDirs) {
94
98
  const sharedMemoryDir = path.join(workspaceRoot, "memory");
@@ -102,11 +106,8 @@ export async function ensureMemoryLayout(workspaceRoot, agentDirs) {
102
106
  // Case 1: Full memory symlink (old layout) — migrate to per-subfolder
103
107
  if (await isSymlink(memoryPath)) {
104
108
  log.info(`migrating full memory symlink to per-subfolder layout: ${agentDir}`);
105
- // The symlink points to the shared memory dir — data is already there.
106
- // Remove the full symlink and create a physical dir with per-subfolder symlinks.
107
109
  await fs.unlink(memoryPath);
108
110
  await fs.mkdir(memoryPath, { recursive: true });
109
- // Data is already in the shared location, just create symlinks and physical dirs
110
111
  }
111
112
  // Ensure memory/ is a physical directory
112
113
  if (!(await isDirectory(memoryPath))) {
@@ -134,11 +135,10 @@ export async function ensureMemoryLayout(workspaceRoot, agentDirs) {
134
135
  /* ignore if race */
135
136
  });
136
137
  }
137
- // Ensure physical dirs for agent-specific data
138
+ // admin/ is the only per-agent physical directory — exists only in admin workspace
138
139
  if (isAdmin) {
139
140
  await fs.mkdir(path.join(memoryPath, "admin"), { recursive: true });
140
141
  }
141
- await fs.mkdir(path.join(memoryPath, "users"), { recursive: true });
142
142
  // Remove admin/ from non-admin agent workspaces (it should never exist there)
143
143
  if (!isAdmin) {
144
144
  const adminPath = path.join(memoryPath, "admin");
@@ -147,13 +147,69 @@ export async function ensureMemoryLayout(workspaceRoot, agentDirs) {
147
147
  await fs.rm(adminPath, { recursive: true }).catch(() => { });
148
148
  }
149
149
  else {
150
- log.warn(`non-admin agent has data in memory/admin/ — moving to shared admin location`);
151
- const sharedAdminPath = path.join(sharedMemoryDir, "admin");
152
- await fs.mkdir(sharedAdminPath, { recursive: true });
153
- await mergeDir(adminPath, sharedAdminPath);
150
+ log.warn(`non-admin agent has data in memory/admin/ — moving to admin agent`);
151
+ const adminAgent = agentDirs.find((a) => a.id.toLowerCase() === "admin" || a.id.toLowerCase().endsWith("-admin"));
152
+ if (adminAgent) {
153
+ const adminMemAdmin = path.join(adminAgent.dir, "memory", "admin");
154
+ await fs.mkdir(adminMemAdmin, { recursive: true });
155
+ await mergeDir(adminPath, adminMemAdmin);
156
+ }
154
157
  await fs.rm(adminPath, { recursive: true }).catch(() => { });
155
158
  }
156
159
  }
157
160
  }
158
161
  }
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.
165
+ await migrateOrphanedSharedData(sharedMemoryDir, agentDirs);
166
+ }
167
+ /**
168
+ * Move admin-only data that was left at the shared memory root
169
+ * (from the old full-symlink layout) into the admin agent's workspace.
170
+ *
171
+ * Shared folders (public, shared, users, groups) belong at the shared root.
172
+ * Everything else (admin, notes, loose .md files) is admin-only.
173
+ */
174
+ async function migrateOrphanedSharedData(sharedMemoryDir, agentDirs) {
175
+ const adminAgent = agentDirs.find((a) => a.id.toLowerCase() === "admin" || a.id.toLowerCase().endsWith("-admin"));
176
+ if (!adminAgent)
177
+ return;
178
+ const adminMemoryPath = path.join(adminAgent.dir, "memory");
179
+ let entries;
180
+ try {
181
+ entries = await fs.readdir(sharedMemoryDir);
182
+ }
183
+ catch {
184
+ return;
185
+ }
186
+ const sharedSet = new Set(SHARED_FOLDERS);
187
+ for (const name of entries) {
188
+ // Skip shared folders — they belong here
189
+ if (sharedSet.has(name))
190
+ continue;
191
+ // Skip hidden files
192
+ if (name.startsWith("."))
193
+ continue;
194
+ const srcPath = path.join(sharedMemoryDir, name);
195
+ if (await isDirectory(srcPath)) {
196
+ const destPath = path.join(adminMemoryPath, name);
197
+ if (await isEmptyDir(srcPath)) {
198
+ await fs.rm(srcPath, { recursive: true }).catch(() => { });
199
+ continue;
200
+ }
201
+ log.info(`migrating orphaned ${name}/ from shared root to admin agent`);
202
+ await mergeDir(srcPath, destPath);
203
+ if (await isEmptyDir(srcPath)) {
204
+ await fs.rm(srcPath, { recursive: true }).catch(() => { });
205
+ }
206
+ }
207
+ else if (name.endsWith(".md")) {
208
+ const destPath = path.join(adminMemoryPath, name);
209
+ if (!(await exists(destPath))) {
210
+ log.info(`migrating orphaned ${name} from shared root to admin agent`);
211
+ await fs.rename(srcPath, destPath);
212
+ }
213
+ }
214
+ }
159
215
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/taskmaster",
3
- "version": "1.0.89",
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 ""