@rubytech/taskmaster 1.0.88 → 1.0.89

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.88",
3
- "commit": "09801d064e3c5ec3d028dd4aaa38a18397e1cbc6",
4
- "builtAt": "2026-02-20T23:50:07.354Z"
2
+ "version": "1.0.89",
3
+ "commit": "d12721761c03ecc5631483c59e1696d0ddba08a3",
4
+ "builtAt": "2026-02-21T00:01:17.386Z"
5
5
  }
@@ -10,6 +10,7 @@ import os from "node:os";
10
10
  import path from "node:path";
11
11
  import { loadConfig, writeConfigFile } from "../config/io.js";
12
12
  import { resolveConfigPath, DEFAULT_GATEWAY_PORT } from "../config/paths.js";
13
+ import { ensureMemoryLayout } from "../memory/layout.js";
13
14
  import { runCommandWithTimeout } from "../process/exec.js";
14
15
  import { resolveTaskmasterPackageRoot } from "../infra/taskmaster-root.js";
15
16
  import { buildSeedConfig, buildDefaultAgentList } from "./provision-seed.js";
@@ -47,6 +48,8 @@ async function runProvision(opts) {
47
48
  await copyTemplateWorkspace(workspace, force);
48
49
  // Step 3: Write agent list to config
49
50
  await writeAgentList(workspace);
51
+ // Step 3b: Set up shared memory layout with per-subfolder symlinks
52
+ await setupMemoryLayout(workspace);
50
53
  // Step 4-6: Linux-only mDNS setup
51
54
  if (skipPlatform) {
52
55
  console.log("[4/7] Avahi: handled by install script");
@@ -154,6 +157,23 @@ async function writeAgentList(workspace) {
154
157
  console.log("[3/7] Agent list: written (public + admin)");
155
158
  }
156
159
  // ---------------------------------------------------------------------------
160
+ // Step 3b: Memory layout
161
+ // ---------------------------------------------------------------------------
162
+ async function setupMemoryLayout(workspace) {
163
+ const agents = buildDefaultAgentList(workspace);
164
+ const agentDirs = agents.map((a) => ({
165
+ dir: a.workspace ?? path.join(workspace, "agents", a.id),
166
+ id: a.id,
167
+ }));
168
+ try {
169
+ await ensureMemoryLayout(workspace, agentDirs);
170
+ console.log("[3b/7] Memory layout: shared public/shared with per-agent symlinks");
171
+ }
172
+ catch (err) {
173
+ console.error(`[3b/7] Memory layout failed: ${String(err)}`);
174
+ }
175
+ }
176
+ // ---------------------------------------------------------------------------
157
177
  // Step 4: Install avahi/mDNS (Linux only)
158
178
  // ---------------------------------------------------------------------------
159
179
  async function installAvahi() {
@@ -168,9 +168,10 @@ function resolveTemplateDir() {
168
168
  /** Copy directory tree recursively, skipping symlink targets for memory (recreates symlinks). */
169
169
  async function cloneWorkspaceStructure(sourceDir, destDir) {
170
170
  await fs.mkdir(destDir, { recursive: true });
171
- // Copy agents/ (recreate memory symlinks)
171
+ // Copy agents/ (skip memory — handled by ensureMemoryLayout)
172
172
  const agentsSrc = path.join(sourceDir, "agents");
173
173
  const agentsDest = path.join(destDir, "agents");
174
+ const clonedAgents = [];
174
175
  try {
175
176
  const agentEntries = await fs.readdir(agentsSrc, { withFileTypes: true });
176
177
  for (const entry of agentEntries) {
@@ -179,32 +180,20 @@ async function cloneWorkspaceStructure(sourceDir, destDir) {
179
180
  const srcAgentDir = path.join(agentsSrc, entry.name);
180
181
  const destAgentDir = path.join(agentsDest, entry.name);
181
182
  await fs.mkdir(destAgentDir, { recursive: true });
182
- // Copy files in agent dir (skip memory symlink — we'll recreate it)
183
+ clonedAgents.push({ dir: destAgentDir, id: entry.name });
184
+ // Copy non-symlink, non-memory files from agent dir
183
185
  const agentFiles = await fs.readdir(srcAgentDir, { withFileTypes: true });
184
- let hasMemoryLink = false;
185
186
  for (const file of agentFiles) {
187
+ if (file.name === "memory")
188
+ continue;
189
+ if (file.isSymbolicLink())
190
+ continue;
186
191
  const srcPath = path.join(srcAgentDir, file.name);
187
192
  const destPath = path.join(destAgentDir, file.name);
188
- if (file.isSymbolicLink()) {
189
- // Memory symlink — recreate pointing to same relative target
190
- const target = await fs.readlink(srcPath);
191
- await fs.symlink(target, destPath).catch(() => {
192
- /* ignore if exists */
193
- });
194
- if (file.name === "memory")
195
- hasMemoryLink = true;
196
- }
197
- else if (file.isFile()) {
193
+ if (file.isFile()) {
198
194
  await fs.copyFile(srcPath, destPath);
199
195
  }
200
196
  }
201
- // Always ensure memory symlink exists (templates may not include them)
202
- if (!hasMemoryLink) {
203
- const memoryLink = path.join(destAgentDir, "memory");
204
- await fs.symlink("../../memory", memoryLink).catch(() => {
205
- /* ignore if exists */
206
- });
207
- }
208
197
  }
209
198
  }
210
199
  catch {
@@ -224,12 +213,11 @@ async function cloneWorkspaceStructure(sourceDir, destDir) {
224
213
  catch {
225
214
  /* source has no skills/ — skip */
226
215
  }
227
- // Create empty memory structure
228
- const memoryDir = path.join(destDir, "memory");
229
- await fs.mkdir(path.join(memoryDir, "public"), { recursive: true });
230
- await fs.mkdir(path.join(memoryDir, "shared"), { recursive: true });
231
- await fs.mkdir(path.join(memoryDir, "admin"), { recursive: true });
232
- await fs.mkdir(path.join(memoryDir, "users"), { recursive: true });
216
+ // Create shared memory structure with per-subfolder symlinks
217
+ if (clonedAgents.length > 0) {
218
+ const { ensureMemoryLayout } = await import("../../memory/layout.js");
219
+ await ensureMemoryLayout(destDir, clonedAgents);
220
+ }
233
221
  }
234
222
  async function copyDirRecursive(src, dest) {
235
223
  await fs.mkdir(dest, { recursive: true });
@@ -1,7 +1,9 @@
1
1
  import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
2
+ import { listAgentIds, resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
2
3
  import { loadModelCatalog } from "../agents/model-catalog.js";
3
4
  import { getModelRefStatus, resolveConfiguredModelRef, resolveHooksGmailModel, } from "../agents/model-selection.js";
4
5
  import { isTruthyEnvValue } from "../infra/env.js";
6
+ import { ensureMemoryLayout } from "../memory/layout.js";
5
7
  import { startGmailWatcher } from "../hooks/gmail-watcher.js";
6
8
  import { clearInternalHooks, createInternalHookEvent, triggerInternalHook, } from "../hooks/internal-hooks.js";
7
9
  import { loadInternalHooks } from "../hooks/loader.js";
@@ -25,6 +27,9 @@ export async function startGatewaySidecars(params) {
25
27
  void recoverOrphanedSessions()
26
28
  .then(() => stripBase64FromTranscripts())
27
29
  .catch(() => { });
30
+ // Ensure memory directory layout: shared public/shared with per-agent symlinks.
31
+ // Self-healing — fixes broken/missing symlinks from prior versions.
32
+ void ensureMemoryLayoutFromConfig(params.cfg).catch(() => { });
28
33
  // Start Gmail watcher if configured (hooks.gmail.account).
29
34
  if (!isTruthyEnvValue(process.env.TASKMASTER_SKIP_GMAIL_WATCHER)) {
30
35
  try {
@@ -129,3 +134,18 @@ export async function startGatewaySidecars(params) {
129
134
  }
130
135
  return { browserControl, pluginServices };
131
136
  }
137
+ async function ensureMemoryLayoutFromConfig(cfg) {
138
+ const agentIds = listAgentIds(cfg);
139
+ if (agentIds.length < 2)
140
+ return; // Single-agent — no shared layout needed
141
+ const agentDirs = agentIds.map((id) => ({
142
+ dir: resolveAgentWorkspaceDir(cfg, id),
143
+ id,
144
+ }));
145
+ // Derive workspace root from agent dirs (strip /agents/{id} suffix)
146
+ const first = agentDirs[0].dir.replace(/\/+$/, "");
147
+ const match = first.match(/^(.+)\/agents\/[^/]+$/);
148
+ if (!match)
149
+ return; // Not a multi-agent workspace layout
150
+ await ensureMemoryLayout(match[1], agentDirs);
151
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Memory directory layout management.
3
+ *
4
+ * Architecture:
5
+ * ~/taskmaster/memory/ ← shared canonical location
6
+ * public/ ← shared: both agents read/write
7
+ * shared/ ← shared: both agents read/write
8
+ * ~/taskmaster/agents/admin/memory/ ← physical directory
9
+ * admin/ ← physical: admin-only data
10
+ * public → ../../../memory/public (symlink to shared)
11
+ * shared → ../../../memory/shared (symlink to shared)
12
+ * users/ ← physical: admin's per-user data
13
+ * ~/taskmaster/agents/public/memory/ ← physical directory
14
+ * public → ../../../memory/public (symlink to shared)
15
+ * shared → ../../../memory/shared (symlink to shared)
16
+ * users/ ← physical: public's per-user data
17
+ *
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).
21
+ */
22
+ import fs from "node:fs/promises";
23
+ import path from "node:path";
24
+ import { createSubsystemLogger } from "../logging/subsystem.js";
25
+ const log = createSubsystemLogger("memory-layout");
26
+ /** Shared scope folders symlinked from each agent workspace. */
27
+ const SHARED_FOLDERS = ["public", "shared"];
28
+ /** The relative symlink target from agents/{id}/memory/{scope} → ../../../memory/{scope} */
29
+ const SYMLINK_TARGET_PREFIX = "../../../memory";
30
+ async function exists(p) {
31
+ try {
32
+ await fs.access(p);
33
+ return true;
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ async function isSymlink(p) {
40
+ try {
41
+ const stat = await fs.lstat(p);
42
+ return stat.isSymbolicLink();
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ async function isDirectory(p) {
49
+ try {
50
+ const stat = await fs.lstat(p);
51
+ return stat.isDirectory();
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ async function isEmptyDir(p) {
58
+ try {
59
+ const entries = await fs.readdir(p);
60
+ return entries.length === 0;
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ }
66
+ /**
67
+ * Move all files from src directory into dest directory (non-destructive merge).
68
+ * Only moves files/dirs that don't already exist in dest.
69
+ */
70
+ async function mergeDir(src, dest) {
71
+ await fs.mkdir(dest, { recursive: true });
72
+ const entries = await fs.readdir(src, { withFileTypes: true });
73
+ for (const entry of entries) {
74
+ const srcPath = path.join(src, entry.name);
75
+ const destPath = path.join(dest, entry.name);
76
+ if (await exists(destPath))
77
+ continue;
78
+ await fs.rename(srcPath, destPath);
79
+ }
80
+ }
81
+ /**
82
+ * Ensure the correct memory layout for a multi-agent workspace.
83
+ *
84
+ * - Creates the shared `memory/public` and `memory/shared` at workspace root
85
+ * - For each agent dir, ensures `memory/` is a physical directory with
86
+ * symlinks for `public/` and `shared/`, and physical dirs for agent-specific data
87
+ * - Migrates from the old full-symlink layout if found
88
+ * - Consolidates data from broken physical dirs into the shared location
89
+ *
90
+ * @param workspaceRoot The workspace root (e.g. ~/taskmaster)
91
+ * @param agentDirs Agent workspace directories (e.g. [~/taskmaster/agents/admin, ~/taskmaster/agents/public])
92
+ */
93
+ export async function ensureMemoryLayout(workspaceRoot, agentDirs) {
94
+ const sharedMemoryDir = path.join(workspaceRoot, "memory");
95
+ // Create shared canonical directories
96
+ for (const folder of SHARED_FOLDERS) {
97
+ await fs.mkdir(path.join(sharedMemoryDir, folder), { recursive: true });
98
+ }
99
+ for (const { dir: agentDir, id: agentId } of agentDirs) {
100
+ const memoryPath = path.join(agentDir, "memory");
101
+ const isAdmin = agentId.toLowerCase() === "admin" || agentId.toLowerCase().endsWith("-admin");
102
+ // Case 1: Full memory symlink (old layout) — migrate to per-subfolder
103
+ if (await isSymlink(memoryPath)) {
104
+ 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
+ await fs.unlink(memoryPath);
108
+ await fs.mkdir(memoryPath, { recursive: true });
109
+ // Data is already in the shared location, just create symlinks and physical dirs
110
+ }
111
+ // Ensure memory/ is a physical directory
112
+ if (!(await isDirectory(memoryPath))) {
113
+ await fs.mkdir(memoryPath, { recursive: true });
114
+ }
115
+ // For each shared folder: consolidate any physical data, then symlink
116
+ for (const folder of SHARED_FOLDERS) {
117
+ const subPath = path.join(memoryPath, folder);
118
+ const sharedPath = path.join(sharedMemoryDir, folder);
119
+ if (await isSymlink(subPath)) {
120
+ // Already a symlink — correct state
121
+ continue;
122
+ }
123
+ if (await isDirectory(subPath)) {
124
+ // Physical dir exists — merge data into shared location, then replace with symlink
125
+ if (!(await isEmptyDir(subPath))) {
126
+ log.info(`consolidating ${folder}/ data from ${agentDir} into shared location`);
127
+ await mergeDir(subPath, sharedPath);
128
+ }
129
+ await fs.rm(subPath, { recursive: true });
130
+ }
131
+ // Create symlink
132
+ const target = `${SYMLINK_TARGET_PREFIX}/${folder}`;
133
+ await fs.symlink(target, subPath).catch(() => {
134
+ /* ignore if race */
135
+ });
136
+ }
137
+ // Ensure physical dirs for agent-specific data
138
+ if (isAdmin) {
139
+ await fs.mkdir(path.join(memoryPath, "admin"), { recursive: true });
140
+ }
141
+ await fs.mkdir(path.join(memoryPath, "users"), { recursive: true });
142
+ // Remove admin/ from non-admin agent workspaces (it should never exist there)
143
+ if (!isAdmin) {
144
+ const adminPath = path.join(memoryPath, "admin");
145
+ if ((await isDirectory(adminPath)) && !(await isSymlink(adminPath))) {
146
+ if (await isEmptyDir(adminPath)) {
147
+ await fs.rm(adminPath, { recursive: true }).catch(() => { });
148
+ }
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);
154
+ await fs.rm(adminPath, { recursive: true }).catch(() => { });
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/taskmaster",
3
- "version": "1.0.88",
3
+ "version": "1.0.89",
4
4
  "description": "AI-powered business assistant for small businesses",
5
5
  "publishConfig": {
6
6
  "access": "public"