@teamclaws/teamclaw 2026.3.26-2 → 2026.4.2-2

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 (46) hide show
  1. package/README.md +52 -8
  2. package/cli.mjs +538 -224
  3. package/index.ts +76 -27
  4. package/openclaw.plugin.json +53 -28
  5. package/package.json +5 -2
  6. package/skills/teamclaw/SKILL.md +213 -0
  7. package/skills/teamclaw/references/api-quick-ref.md +117 -0
  8. package/skills/teamclaw-setup/SKILL.md +81 -0
  9. package/skills/teamclaw-setup/references/install-modes.md +136 -0
  10. package/skills/teamclaw-setup/references/validation-checklist.md +73 -0
  11. package/src/config.ts +44 -16
  12. package/src/controller/controller-capacity.ts +2 -2
  13. package/src/controller/controller-service.ts +193 -47
  14. package/src/controller/controller-tools.ts +102 -2
  15. package/src/controller/delivery-report.ts +563 -0
  16. package/src/controller/http-server.ts +1907 -172
  17. package/src/controller/kickoff-orchestrator.ts +292 -0
  18. package/src/controller/managed-gateway-process.ts +330 -0
  19. package/src/controller/orchestration-manifest.ts +69 -1
  20. package/src/controller/preview-manager.ts +676 -0
  21. package/src/controller/prompt-injector.ts +116 -67
  22. package/src/controller/role-inference.ts +41 -0
  23. package/src/controller/websocket.ts +3 -1
  24. package/src/controller/worker-provisioning.ts +429 -74
  25. package/src/discovery.ts +1 -1
  26. package/src/git-collaboration.ts +198 -47
  27. package/src/identity.ts +12 -2
  28. package/src/interaction-contracts.ts +179 -3
  29. package/src/networking.ts +99 -0
  30. package/src/openclaw-workspace.ts +478 -11
  31. package/src/prompt-policy.ts +381 -0
  32. package/src/roles.ts +37 -36
  33. package/src/state.ts +40 -1
  34. package/src/task-executor.ts +282 -78
  35. package/src/types.ts +150 -7
  36. package/src/ui/app.js +1403 -175
  37. package/src/ui/assets/teamclaw-app-icon.png +0 -0
  38. package/src/ui/index.html +122 -40
  39. package/src/ui/style.css +829 -143
  40. package/src/worker/http-handler.ts +40 -4
  41. package/src/worker/prompt-injector.ts +9 -38
  42. package/src/worker/skill-installer.ts +2 -2
  43. package/src/worker/tools.ts +31 -5
  44. package/src/worker/worker-service.ts +49 -8
  45. package/src/workspace-browser.ts +20 -7
  46. package/src/controller/local-worker-manager.ts +0 -533
@@ -0,0 +1,99 @@
1
+ import os from "node:os";
2
+ import { spawnSync } from "node:child_process";
3
+
4
+ function isPrivateLanIpv4(address: string): boolean {
5
+ if (address.startsWith("10.") || address.startsWith("192.168.")) {
6
+ return true;
7
+ }
8
+ const parts = address.split(".").map((value) => Number.parseInt(value, 10));
9
+ return parts.length === 4 && parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31;
10
+ }
11
+
12
+ function rankLanAddress(address: string): number {
13
+ if (address.startsWith("192.168.")) {
14
+ return 0;
15
+ }
16
+ if (address.startsWith("10.")) {
17
+ return 1;
18
+ }
19
+ const parts = address.split(".").map((value) => Number.parseInt(value, 10));
20
+ if (parts.length === 4 && parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) {
21
+ return 2;
22
+ }
23
+ return 3;
24
+ }
25
+
26
+ function parseDefaultRouteInterface(text: string): string {
27
+ const directMatch = text.match(/(?:^|\n)\s*interface:\s*(\S+)/i);
28
+ if (directMatch?.[1]) {
29
+ return directMatch[1];
30
+ }
31
+ const devMatch = text.match(/(?:^|\n)default(?:\s+via\s+\S+)?\s+dev\s+(\S+)/i);
32
+ if (devMatch?.[1]) {
33
+ return devMatch[1];
34
+ }
35
+ return "";
36
+ }
37
+
38
+ function probeDefaultRouteInterface(): string {
39
+ const candidates: Array<{ command: string; args: string[] }> = process.platform === "darwin"
40
+ ? [
41
+ { command: "route", args: ["-n", "get", "default"] },
42
+ { command: "ip", args: ["route", "show", "default"] },
43
+ ]
44
+ : [
45
+ { command: "ip", args: ["route", "show", "default"] },
46
+ { command: "route", args: ["-n", "get", "default"] },
47
+ ];
48
+ for (const candidate of candidates) {
49
+ const result = spawnSync(candidate.command, candidate.args, { encoding: "utf8" });
50
+ if (result.status !== 0 || result.error) {
51
+ continue;
52
+ }
53
+ const interfaceName = parseDefaultRouteInterface(result.stdout || "");
54
+ if (interfaceName) {
55
+ return interfaceName;
56
+ }
57
+ }
58
+ return "";
59
+ }
60
+
61
+ function listNonInternalIpv4Addresses(): string[] {
62
+ const addresses: string[] = [];
63
+ const interfaces = os.networkInterfaces();
64
+ for (const records of Object.values(interfaces)) {
65
+ for (const record of records ?? []) {
66
+ if (!record || record.internal || record.family !== "IPv4") {
67
+ continue;
68
+ }
69
+ addresses.push(record.address);
70
+ }
71
+ }
72
+ return addresses.filter((value, index, values) => values.indexOf(value) === index);
73
+ }
74
+
75
+ export function resolvePreferredLanAddress(): string | null {
76
+ const interfaces = os.networkInterfaces();
77
+ const defaultRouteInterface = probeDefaultRouteInterface();
78
+ if (defaultRouteInterface) {
79
+ const records = (interfaces[defaultRouteInterface] ?? [])
80
+ .filter((record): record is os.NetworkInterfaceInfoIPv4 => Boolean(record) && record.family === "IPv4" && !record.internal);
81
+ const preferredPrivate = records.find((record) => isPrivateLanIpv4(record.address));
82
+ if (preferredPrivate) {
83
+ return preferredPrivate.address;
84
+ }
85
+ if (records[0]) {
86
+ return records[0].address;
87
+ }
88
+ }
89
+
90
+ const privateCandidates = listNonInternalIpv4Addresses()
91
+ .filter((address) => isPrivateLanIpv4(address))
92
+ .sort((left, right) => rankLanAddress(left) - rankLanAddress(right) || left.localeCompare(right));
93
+ if (privateCandidates[0]) {
94
+ return privateCandidates[0];
95
+ }
96
+
97
+ const fallback = listNonInternalIpv4Addresses()[0];
98
+ return fallback || null;
99
+ }
@@ -1,10 +1,16 @@
1
- import { readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import JSON5 from "json5";
6
6
  import type { PluginLogger } from "../api.js";
7
7
 
8
+ export const TEAMCLAW_AGENT_ID = "teamclaw";
9
+ export const TEAMCLAW_ISOLATION_MODE_INDEPENDENT = "independent";
10
+ export const TEAMCLAW_ISOLATION_MODE_MAIN = "main";
11
+ const TEAMCLAW_RECOMMENDED_EXEC_SECURITY = "full";
12
+ const TEAMCLAW_RECOMMENDED_EXEC_ASK = "off";
13
+
8
14
  const DEFAULT_AGENTS_MD = `# AGENTS.md
9
15
 
10
16
  This workspace is shared by TeamClaw controller and workers.
@@ -12,6 +18,8 @@ This workspace is shared by TeamClaw controller and workers.
12
18
  Rules:
13
19
  - Treat task-provided file paths as hints; verify they exist before reading or editing.
14
20
  - Use the shared \`memory/\` directory for lightweight notes when useful.
21
+ - Check \`memory/patterns.md\` for previously discovered codebase patterns before starting work.
22
+ - Check for \`.teamclaw-notes.md\` files in directories you work on for prior context.
15
23
  - Report meaningful progress during longer tasks.
16
24
  - If requirements or environment details are missing and work cannot continue safely, request clarification instead of guessing.
17
25
  `;
@@ -31,10 +39,25 @@ const DEFAULT_HEARTBEAT_MD = `# HEARTBEAT.md
31
39
  # Keep this file empty (or with only comments) to skip heartbeat API calls.
32
40
  `;
33
41
 
42
+ const DEFAULT_PATTERNS_MD = `# Codebase Patterns
43
+
44
+ Reusable patterns discovered by TeamClaw workers during task execution.
45
+ This file is automatically maintained — new patterns are appended as workers complete tasks.
46
+ Read this file before starting work to benefit from previously discovered knowledge.
47
+ `;
48
+
34
49
  function isRecord(value: unknown): value is Record<string, unknown> {
35
50
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
36
51
  }
37
52
 
53
+ function normalizeAgentId(value: unknown): string {
54
+ const trimmed = typeof value === "string" ? value.trim().toLowerCase() : "";
55
+ if (!trimmed) {
56
+ return "main";
57
+ }
58
+ return trimmed.replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "main";
59
+ }
60
+
38
61
  function expandUserPath(
39
62
  value: string,
40
63
  homedir: () => string = os.homedir,
@@ -56,24 +79,139 @@ function resolveConfiguredOpenClawWorkspaceDir(
56
79
  env: NodeJS.ProcessEnv = process.env,
57
80
  homedir: () => string = os.homedir,
58
81
  ): string {
82
+ const parsed = loadOpenClawConfigRecord(env, homedir);
83
+ if (!parsed) {
84
+ return "";
85
+ }
86
+ const agents = isRecord(parsed.agents) ? parsed.agents : null;
87
+ const defaults = agents && isRecord(agents.defaults) ? agents.defaults : null;
88
+ if (defaults && typeof defaults.workspace === "string" && defaults.workspace.trim()) {
89
+ return expandUserPath(defaults.workspace, homedir);
90
+ }
91
+ return "";
92
+ }
93
+
94
+ function resolveConfiguredTeamClawIsolationMode(
95
+ env: NodeJS.ProcessEnv = process.env,
96
+ homedir: () => string = os.homedir,
97
+ ): string {
98
+ const parsed = loadOpenClawConfigRecord(env, homedir);
99
+ if (!parsed) {
100
+ return TEAMCLAW_ISOLATION_MODE_INDEPENDENT;
101
+ }
102
+ const plugins = isRecord(parsed.plugins) ? parsed.plugins : null;
103
+ const entries = plugins && isRecord(plugins.entries) ? plugins.entries : null;
104
+ const teamclawEntry = entries && isRecord(entries[TEAMCLAW_AGENT_ID]) ? entries[TEAMCLAW_AGENT_ID] : null;
105
+ const teamclawConfig = teamclawEntry && isRecord(teamclawEntry.config) ? teamclawEntry.config : null;
106
+ return teamclawConfig?.agentIsolationMode === TEAMCLAW_ISOLATION_MODE_MAIN
107
+ ? TEAMCLAW_ISOLATION_MODE_MAIN
108
+ : TEAMCLAW_ISOLATION_MODE_INDEPENDENT;
109
+ }
110
+
111
+ function resolveExplicitTeamClawWorkspaceDir(
112
+ env: NodeJS.ProcessEnv = process.env,
113
+ homedir: () => string = os.homedir,
114
+ ): string {
115
+ const override = env.TEAMCLAW_WORKSPACE_DIR?.trim();
116
+ if (!override) {
117
+ return "";
118
+ }
119
+ return expandUserPath(override, homedir);
120
+ }
121
+
122
+ function loadOpenClawConfigRecord(
123
+ env: NodeJS.ProcessEnv = process.env,
124
+ homedir: () => string = os.homedir,
125
+ ): Record<string, unknown> | null {
59
126
  const configPath = resolveDefaultOpenClawConfigPath(env, homedir);
60
127
  try {
61
128
  const raw = readFileSync(configPath, "utf8");
62
129
  const parsed = JSON5.parse(raw);
63
- if (!isRecord(parsed)) {
64
- return "";
130
+ return isRecord(parsed) ? parsed : null;
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+
136
+ function resolveConfiguredAgentEntry(
137
+ agentId: string,
138
+ env: NodeJS.ProcessEnv = process.env,
139
+ homedir: () => string = os.homedir,
140
+ ): Record<string, unknown> | null {
141
+ const parsed = loadOpenClawConfigRecord(env, homedir);
142
+ if (!parsed) {
143
+ return null;
144
+ }
145
+ const agents = isRecord(parsed.agents) ? parsed.agents : null;
146
+ const list = agents && Array.isArray(agents.list) ? agents.list : [];
147
+ const normalizedAgentId = normalizeAgentId(agentId);
148
+ for (const entry of list) {
149
+ if (!isRecord(entry)) {
150
+ continue;
65
151
  }
66
- const agents = isRecord(parsed.agents) ? parsed.agents : null;
67
- const defaults = agents && isRecord(agents.defaults) ? agents.defaults : null;
68
- if (defaults && typeof defaults.workspace === "string" && defaults.workspace.trim()) {
69
- return expandUserPath(defaults.workspace, homedir);
152
+ if (normalizeAgentId(entry.id) === normalizedAgentId) {
153
+ return entry;
70
154
  }
71
- } catch {
72
- // Fall back to the legacy state-dir-derived workspace path below.
155
+ }
156
+ return null;
157
+ }
158
+
159
+ function resolveConfiguredDefaultModelValue(
160
+ env: NodeJS.ProcessEnv = process.env,
161
+ homedir: () => string = os.homedir,
162
+ ): string {
163
+ const parsed = loadOpenClawConfigRecord(env, homedir);
164
+ if (!parsed) {
165
+ return "";
166
+ }
167
+ const agents = isRecord(parsed.agents) ? parsed.agents : null;
168
+ const defaults = agents && isRecord(agents.defaults) ? agents.defaults : null;
169
+ const model = defaults?.model;
170
+ if (typeof model === "string") {
171
+ return model.trim();
172
+ }
173
+ if (isRecord(model) && typeof model.primary === "string") {
174
+ return model.primary.trim();
73
175
  }
74
176
  return "";
75
177
  }
76
178
 
179
+ function resolveConfiguredTeamClawModelValue(
180
+ env: NodeJS.ProcessEnv = process.env,
181
+ homedir: () => string = os.homedir,
182
+ ): string {
183
+ const teamclawEntry = resolveConfiguredAgentEntry(TEAMCLAW_AGENT_ID, env, homedir);
184
+ const model = teamclawEntry?.model;
185
+ if (typeof model === "string") {
186
+ return model.trim();
187
+ }
188
+ if (isRecord(model) && typeof model.primary === "string") {
189
+ return model.primary.trim();
190
+ }
191
+ return resolveConfiguredDefaultModelValue(env, homedir);
192
+ }
193
+
194
+ function cloneJsonValue<T>(value: T): T {
195
+ return value == null ? value : JSON.parse(JSON.stringify(value)) as T;
196
+ }
197
+
198
+ function resolveConfiguredDefaultAgentId(
199
+ env: NodeJS.ProcessEnv = process.env,
200
+ homedir: () => string = os.homedir,
201
+ ): string {
202
+ const parsed = loadOpenClawConfigRecord(env, homedir);
203
+ if (!parsed) {
204
+ return "main";
205
+ }
206
+ const agents = isRecord(parsed.agents) ? parsed.agents : null;
207
+ const list = agents && Array.isArray(agents.list) ? agents.list.filter(isRecord) : [];
208
+ if (list.length === 0) {
209
+ return "main";
210
+ }
211
+ const defaultEntry = list.find((entry) => entry.default === true) ?? list[0];
212
+ return normalizeAgentId(defaultEntry.id);
213
+ }
214
+
77
215
  export function resolveDefaultOpenClawHomeDir(
78
216
  env: NodeJS.ProcessEnv = process.env,
79
217
  homedir: () => string = os.homedir,
@@ -122,15 +260,207 @@ export function resolveDefaultOpenClawWorkspaceDir(
122
260
  return path.join(stateDir, "workspace");
123
261
  }
124
262
 
263
+ export function resolveTeamClawAgentDir(
264
+ env: NodeJS.ProcessEnv = process.env,
265
+ homedir: () => string = os.homedir,
266
+ ): string {
267
+ if (resolveConfiguredTeamClawIsolationMode(env, homedir) === TEAMCLAW_ISOLATION_MODE_MAIN) {
268
+ return resolveDefaultAgentDir(env, homedir);
269
+ }
270
+ const configuredEntry = resolveConfiguredAgentEntry(TEAMCLAW_AGENT_ID, env, homedir);
271
+ if (configuredEntry && typeof configuredEntry.agentDir === "string" && configuredEntry.agentDir.trim()) {
272
+ return expandUserPath(configuredEntry.agentDir, homedir);
273
+ }
274
+ return path.join(resolveDefaultOpenClawStateDir(env, homedir), "agents", TEAMCLAW_AGENT_ID, "agent");
275
+ }
276
+
277
+ export type TeamClawModelReadiness = {
278
+ status: "ready" | "missing";
279
+ hasConfiguredModel: boolean;
280
+ hasAuthProfiles: boolean;
281
+ configuredModel: string;
282
+ authPath: string;
283
+ message: string;
284
+ };
285
+
286
+ export function getTeamClawModelReadiness(
287
+ env: NodeJS.ProcessEnv = process.env,
288
+ homedir: () => string = os.homedir,
289
+ ): TeamClawModelReadiness {
290
+ const configuredModel = resolveConfiguredTeamClawModelValue(env, homedir);
291
+ const authCandidates = [
292
+ path.join(resolveTeamClawAgentDir(env, homedir), "auth-profiles.json"),
293
+ path.join(resolveDefaultAgentDir(env, homedir), "auth-profiles.json"),
294
+ ].filter((value, index, values) => values.indexOf(value) === index);
295
+ const authPath = authCandidates.find((candidatePath) => {
296
+ try {
297
+ return existsSync(candidatePath);
298
+ } catch {
299
+ return false;
300
+ }
301
+ }) ?? authCandidates[0] ?? "";
302
+ const hasConfiguredModel = Boolean(configuredModel);
303
+ const hasAuthProfiles = Boolean(authPath) && existsSync(authPath);
304
+ const status = hasConfiguredModel && hasAuthProfiles ? "ready" : "missing";
305
+ const missingParts = [];
306
+ if (!hasConfiguredModel) {
307
+ missingParts.push("no model is configured");
308
+ }
309
+ if (!hasAuthProfiles) {
310
+ missingParts.push("no auth-profiles.json was found");
311
+ }
312
+ return {
313
+ status,
314
+ hasConfiguredModel,
315
+ hasAuthProfiles,
316
+ configuredModel,
317
+ authPath,
318
+ message: status === "ready"
319
+ ? `TeamClaw is ready with model ${configuredModel}.`
320
+ : `TeamClaw cannot work yet because ${missingParts.join(" and ")}.`,
321
+ };
322
+ }
323
+
324
+ export function resolveDefaultAgentDir(
325
+ env: NodeJS.ProcessEnv = process.env,
326
+ homedir: () => string = os.homedir,
327
+ ): string {
328
+ const defaultAgentId = resolveConfiguredDefaultAgentId(env, homedir);
329
+ const configuredEntry = resolveConfiguredAgentEntry(defaultAgentId, env, homedir);
330
+ if (configuredEntry && typeof configuredEntry.agentDir === "string" && configuredEntry.agentDir.trim()) {
331
+ return expandUserPath(configuredEntry.agentDir, homedir);
332
+ }
333
+ return path.join(resolveDefaultOpenClawStateDir(env, homedir), "agents", defaultAgentId, "agent");
334
+ }
335
+
336
+ export function resolveTeamClawAgentWorkspaceRootDir(
337
+ env: NodeJS.ProcessEnv = process.env,
338
+ homedir: () => string = os.homedir,
339
+ ): string {
340
+ const explicitWorkspaceDir = resolveExplicitTeamClawWorkspaceDir(env, homedir);
341
+ if (explicitWorkspaceDir) {
342
+ return explicitWorkspaceDir;
343
+ }
344
+ if (resolveConfiguredTeamClawIsolationMode(env, homedir) === TEAMCLAW_ISOLATION_MODE_MAIN) {
345
+ return resolveDefaultOpenClawWorkspaceDir(env, homedir);
346
+ }
347
+ const configuredEntry = resolveConfiguredAgentEntry(TEAMCLAW_AGENT_ID, env, homedir);
348
+ if (configuredEntry && typeof configuredEntry.workspace === "string" && configuredEntry.workspace.trim()) {
349
+ return expandUserPath(configuredEntry.workspace, homedir);
350
+ }
351
+ return path.join(resolveDefaultOpenClawStateDir(env, homedir), `workspace-${TEAMCLAW_AGENT_ID}`);
352
+ }
353
+
354
+ export function buildTeamClawAgentSessionKey(
355
+ input: string,
356
+ env: NodeJS.ProcessEnv = process.env,
357
+ homedir: () => string = os.homedir,
358
+ ): string {
359
+ const trimmed = input.trim();
360
+ const agentMatch = trimmed.match(/^agent:[^:]+:(.+)$/i);
361
+ const logicalKey = (agentMatch?.[1] ?? trimmed).trim().toLowerCase() || "main";
362
+ if (resolveConfiguredTeamClawIsolationMode(env, homedir) === TEAMCLAW_ISOLATION_MODE_MAIN) {
363
+ return logicalKey;
364
+ }
365
+ return `agent:${TEAMCLAW_AGENT_ID}:${logicalKey}`;
366
+ }
367
+
125
368
  export function resolveDefaultTeamClawRuntimeRootDir(
126
369
  env: NodeJS.ProcessEnv = process.env,
127
370
  homedir: () => string = os.homedir,
128
371
  ): string {
129
- return path.join(path.dirname(resolveDefaultOpenClawWorkspaceDir(env, homedir)), "teamclaw-runtimes");
372
+ return path.join(resolveDefaultOpenClawStateDir(env, homedir), "teamclaw-runtimes");
373
+ }
374
+
375
+ /**
376
+ * TeamClaw-specific workspace directory.
377
+ *
378
+ * By default this resolves to a dedicated OpenClaw agent workspace sibling such as
379
+ * `<stateDir>/workspace-teamclaw`.
380
+ *
381
+ * When `agentIsolationMode` is set to `main`, TeamClaw falls back to the legacy
382
+ * shared layout and uses `<default-workspace>/teamclaw`.
383
+ */
384
+ export function resolveTeamClawWorkspaceDir(
385
+ env: NodeJS.ProcessEnv = process.env,
386
+ homedir: () => string = os.homedir,
387
+ ): string {
388
+ if (resolveConfiguredTeamClawIsolationMode(env, homedir) === TEAMCLAW_ISOLATION_MODE_MAIN) {
389
+ return path.join(resolveTeamClawAgentWorkspaceRootDir(env, homedir), TEAMCLAW_AGENT_ID);
390
+ }
391
+ return resolveTeamClawAgentWorkspaceRootDir(env, homedir);
392
+ }
393
+
394
+ /**
395
+ * Resolve the `projects/` root inside the TeamClaw workspace.
396
+ * Each orchestration run or ad-hoc task gets its own subdirectory here.
397
+ */
398
+ export function resolveTeamClawProjectsDir(
399
+ env?: NodeJS.ProcessEnv,
400
+ homedir?: () => string,
401
+ ): string {
402
+ return path.join(resolveTeamClawWorkspaceDir(env, homedir), "projects");
403
+ }
404
+
405
+ /**
406
+ * Resolve a project path relative to the active TeamClaw workspace.
407
+ * Workers operate inside the TeamClaw workspace itself, so project-local file
408
+ * operations should use `projects/<projectDir>` regardless of isolation mode.
409
+ */
410
+ export function buildTeamClawProjectWorkspacePath(projectDir: string): string {
411
+ const normalized = String(projectDir || "").trim().replace(/^\/+|\/+$/gu, "");
412
+ return normalized ? `projects/${normalized}` : "projects";
413
+ }
414
+
415
+ /**
416
+ * Resolve a project path relative to the TeamClaw agent workspace root.
417
+ * Preview metadata is evaluated from the agent workspace root, which differs
418
+ * between `independent` and `main` isolation modes.
419
+ */
420
+ export function buildTeamClawProjectAgentRelativePath(
421
+ projectDir: string,
422
+ env: NodeJS.ProcessEnv = process.env,
423
+ homedir: () => string = os.homedir,
424
+ ): string {
425
+ const workspaceRelative = buildTeamClawProjectWorkspacePath(projectDir);
426
+ const agentWorkspaceRoot = resolveTeamClawAgentWorkspaceRootDir(env, homedir);
427
+ const teamclawWorkspaceDir = resolveTeamClawWorkspaceDir(env, homedir);
428
+ const absoluteProjectPath = path.join(teamclawWorkspaceDir, workspaceRelative);
429
+ const relativeToAgentRoot = path.relative(agentWorkspaceRoot, absoluteProjectPath).replace(/\\/gu, "/");
430
+ return relativeToAgentRoot || ".";
431
+ }
432
+
433
+ /**
434
+ * Derive a filesystem-safe project slug from free-form text.
435
+ *
436
+ * 1. Lower-case, replace non-alphanumeric runs with hyphens, trim to ~50 chars.
437
+ * 2. Append a short random suffix to avoid collisions.
438
+ *
439
+ * Example: "Build a payment system with Stripe" → "build-a-payment-system-with-stripe-k3f9m2"
440
+ */
441
+ export function deriveProjectSlug(text: string): string {
442
+ const slug = text
443
+ .toLowerCase()
444
+ .replace(/[^a-z0-9]+/g, "-")
445
+ .replace(/^-+|-+$/g, "")
446
+ .slice(0, 50)
447
+ .replace(/-+$/, "");
448
+ const suffix = randomSuffix(6);
449
+ return slug ? `${slug}-${suffix}` : suffix;
450
+ }
451
+
452
+ function randomSuffix(length: number): string {
453
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
454
+ let result = "";
455
+ for (let i = 0; i < length; i++) {
456
+ result += chars[Math.floor(Math.random() * chars.length)];
457
+ }
458
+ return result;
130
459
  }
131
460
 
132
461
  export async function ensureOpenClawWorkspaceMemoryDir(logger: PluginLogger): Promise<string> {
133
- const workspaceDir = resolveDefaultOpenClawWorkspaceDir();
462
+ await ensureTeamClawAgentBootstrap(logger);
463
+ const workspaceDir = resolveTeamClawWorkspaceDir();
134
464
  const memoryDir = path.join(workspaceDir, "memory");
135
465
  try {
136
466
  await fs.mkdir(workspaceDir, { recursive: true });
@@ -138,6 +468,7 @@ export async function ensureOpenClawWorkspaceMemoryDir(logger: PluginLogger): Pr
138
468
  await ensureFileIfMissing(path.join(workspaceDir, "AGENTS.md"), DEFAULT_AGENTS_MD);
139
469
  await ensureFileIfMissing(path.join(workspaceDir, "BOOTSTRAP.md"), DEFAULT_BOOTSTRAP_MD);
140
470
  await ensureFileIfMissing(path.join(workspaceDir, "HEARTBEAT.md"), DEFAULT_HEARTBEAT_MD);
471
+ await ensureFileIfMissing(path.join(memoryDir, "patterns.md"), DEFAULT_PATTERNS_MD);
141
472
  } catch (err) {
142
473
  logger.warn(
143
474
  `TeamClaw: failed to ensure OpenClaw workspace memory dir at ${memoryDir}: ${
@@ -148,6 +479,142 @@ export async function ensureOpenClawWorkspaceMemoryDir(logger: PluginLogger): Pr
148
479
  return memoryDir;
149
480
  }
150
481
 
482
+ export async function ensureTeamClawAgentBootstrap(logger: PluginLogger): Promise<void> {
483
+ const teamclawAgentDir = resolveTeamClawAgentDir();
484
+ const teamclawWorkspaceRoot = resolveTeamClawAgentWorkspaceRootDir();
485
+ const isolationMode = resolveConfiguredTeamClawIsolationMode();
486
+ try {
487
+ await fs.mkdir(teamclawWorkspaceRoot, { recursive: true });
488
+ if (isolationMode === TEAMCLAW_ISOLATION_MODE_MAIN) {
489
+ return;
490
+ }
491
+ await ensureTeamClawAgentConfigBootstrap(logger);
492
+ await fs.mkdir(teamclawAgentDir, { recursive: true });
493
+ const sourceAgentDirs = [
494
+ resolveDefaultAgentDir(),
495
+ path.join(resolveDefaultOpenClawStateDir(), "agents", "main", "agent"),
496
+ ].filter((value, index, values) => values.indexOf(value) === index);
497
+ for (const sourceAgentDir of sourceAgentDirs) {
498
+ const sourceAuthProfilesPath = path.join(sourceAgentDir, "auth-profiles.json");
499
+ try {
500
+ await fs.access(sourceAuthProfilesPath);
501
+ } catch {
502
+ continue;
503
+ }
504
+ const targetAuthProfilesPath = path.join(teamclawAgentDir, "auth-profiles.json");
505
+ await fs.copyFile(sourceAuthProfilesPath, targetAuthProfilesPath);
506
+ logger.info(`TeamClaw: bootstrapped dedicated agent auth from ${sourceAuthProfilesPath}`);
507
+ break;
508
+ }
509
+ } catch (err) {
510
+ logger.warn(
511
+ `TeamClaw: failed to bootstrap dedicated agent runtime: ${err instanceof Error ? err.message : String(err)}`,
512
+ );
513
+ }
514
+ }
515
+
516
+ async function ensureTeamClawAgentConfigBootstrap(logger: PluginLogger): Promise<void> {
517
+ const configPath = resolveDefaultOpenClawConfigPath();
518
+ let raw: string;
519
+ try {
520
+ raw = await fs.readFile(configPath, "utf8");
521
+ } catch {
522
+ return;
523
+ }
524
+
525
+ let parsed: Record<string, unknown>;
526
+ try {
527
+ const candidate = JSON5.parse(raw);
528
+ if (!isRecord(candidate)) {
529
+ return;
530
+ }
531
+ parsed = candidate;
532
+ } catch {
533
+ return;
534
+ }
535
+
536
+ const agents = isRecord(parsed.agents) ? parsed.agents : {};
537
+ const defaults = isRecord(agents.defaults) ? agents.defaults : {};
538
+ const nextList = Array.isArray(agents.list) ? [...agents.list] : [];
539
+ const rootTools = isRecord(parsed.tools) ? parsed.tools : {};
540
+ const rootExec = isRecord(rootTools.exec) ? rootTools.exec : {};
541
+ const plugins = isRecord(parsed.plugins) ? parsed.plugins : {};
542
+ const entries = isRecord(plugins.entries) ? plugins.entries : {};
543
+ const teamclawPluginEntry = isRecord(entries[TEAMCLAW_AGENT_ID]) ? entries[TEAMCLAW_AGENT_ID] : {};
544
+ const existingPluginConfig = isRecord(teamclawPluginEntry.config) ? teamclawPluginEntry.config : {};
545
+ const teamclawWorkspaceDir = path.join(resolveDefaultOpenClawStateDir(), `workspace-${TEAMCLAW_AGENT_ID}`);
546
+ const teamclawAgentDir = path.join(resolveDefaultOpenClawStateDir(), "agents", TEAMCLAW_AGENT_ID, "agent");
547
+ const existingIndex = nextList.findIndex((entry) => isRecord(entry) && normalizeAgentId(entry.id) === TEAMCLAW_AGENT_ID);
548
+ const existingEntry = existingIndex >= 0 && isRecord(nextList[existingIndex]) ? nextList[existingIndex] : {};
549
+ const existingEntryTools = isRecord(existingEntry.tools) ? existingEntry.tools : {};
550
+ const existingEntryExec = isRecord(existingEntryTools.exec) ? existingEntryTools.exec : {};
551
+ const nextEntryExec: Record<string, unknown> = {
552
+ ...cloneJsonValue(rootExec),
553
+ ...cloneJsonValue(existingEntryExec),
554
+ };
555
+ if (typeof nextEntryExec.security !== "string" || !nextEntryExec.security.trim()) {
556
+ nextEntryExec.security = typeof rootExec.security === "string" && rootExec.security.trim()
557
+ ? rootExec.security.trim()
558
+ : TEAMCLAW_RECOMMENDED_EXEC_SECURITY;
559
+ }
560
+ if (typeof nextEntryExec.ask !== "string" || !nextEntryExec.ask.trim()) {
561
+ nextEntryExec.ask = typeof rootExec.ask === "string" && rootExec.ask.trim()
562
+ ? rootExec.ask.trim()
563
+ : TEAMCLAW_RECOMMENDED_EXEC_ASK;
564
+ }
565
+ const nextEntry: Record<string, unknown> = {
566
+ ...existingEntry,
567
+ id: TEAMCLAW_AGENT_ID,
568
+ workspace: typeof existingEntry.workspace === "string" && existingEntry.workspace.trim()
569
+ ? existingEntry.workspace
570
+ : teamclawWorkspaceDir,
571
+ agentDir: typeof existingEntry.agentDir === "string" && existingEntry.agentDir.trim()
572
+ ? existingEntry.agentDir
573
+ : teamclawAgentDir,
574
+ tools: {
575
+ ...cloneJsonValue(existingEntryTools),
576
+ exec: nextEntryExec,
577
+ },
578
+ };
579
+ if (nextEntry.model == null && defaults.model != null) {
580
+ nextEntry.model = cloneJsonValue(defaults.model);
581
+ }
582
+
583
+ const nextPluginConfig: Record<string, unknown> = {
584
+ ...existingPluginConfig,
585
+ };
586
+ if (
587
+ nextPluginConfig.mode === "controller"
588
+ && nextPluginConfig.processModel === "multi"
589
+ && nextPluginConfig.workerProvisioningType === "none"
590
+ && nextPluginConfig.workerProvisioningDisabled !== true
591
+ ) {
592
+ nextPluginConfig.workerProvisioningType = "process";
593
+ }
594
+
595
+ const entryChanged = JSON.stringify(existingEntry) !== JSON.stringify(nextEntry);
596
+ const pluginConfigChanged = JSON.stringify(existingPluginConfig) !== JSON.stringify(nextPluginConfig);
597
+ if (!entryChanged && !pluginConfigChanged) {
598
+ return;
599
+ }
600
+
601
+ if (existingIndex >= 0) {
602
+ nextList[existingIndex] = nextEntry;
603
+ } else {
604
+ nextList.push(nextEntry);
605
+ }
606
+ agents.list = nextList;
607
+ parsed.agents = agents;
608
+ if (pluginConfigChanged) {
609
+ teamclawPluginEntry.config = nextPluginConfig;
610
+ entries[TEAMCLAW_AGENT_ID] = teamclawPluginEntry;
611
+ plugins.entries = entries;
612
+ parsed.plugins = plugins;
613
+ }
614
+ await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
615
+ logger.info(`TeamClaw: bootstrapped dedicated agent config into ${configPath}`);
616
+ }
617
+
151
618
  async function ensureFileIfMissing(filePath: string, content: string): Promise<void> {
152
619
  try {
153
620
  await fs.access(filePath);