velaclaw-dev 0.2.0

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 (63) hide show
  1. package/.gitignore +14 -0
  2. package/ARCHITECTURE.md +143 -0
  3. package/README.dev.md +208 -0
  4. package/README.local-before-remote-sync.md +224 -0
  5. package/README.md +211 -0
  6. package/README.public.md +115 -0
  7. package/RELEASING.md +162 -0
  8. package/TESTING.md +195 -0
  9. package/dist/cli.js +213 -0
  10. package/dist/data.js +2988 -0
  11. package/dist/server.js +1020 -0
  12. package/dist/ui.js +1486 -0
  13. package/members/LAUNCH_CHECKLIST.md +13 -0
  14. package/members/README.md +17 -0
  15. package/members/member-template/README.md +9 -0
  16. package/members/member-template/private-docs/README.md +3 -0
  17. package/members/member-template/private-memory/README.md +3 -0
  18. package/members/member-template/private-skills/README.md +4 -0
  19. package/members/member-template/private-tools/README.md +4 -0
  20. package/members/member-template/runtime/config/README.md +3 -0
  21. package/members/member-template/runtime/config/local-plugins/member-quota-guard/index.js +123 -0
  22. package/members/member-template/runtime/config/local-plugins/member-quota-guard/openclaw.plugin.json +19 -0
  23. package/members/member-template/runtime/config/local-plugins/member-quota-guard/package.json +10 -0
  24. package/members/member-template/runtime/config/local-plugins/member-runtime-upgrader/index.js +97 -0
  25. package/members/member-template/runtime/config/local-plugins/member-runtime-upgrader/openclaw.plugin.json +21 -0
  26. package/members/member-template/runtime/config/local-plugins/member-runtime-upgrader/package.json +10 -0
  27. package/members/member-template/runtime/config/local-plugins/shared-asset-injector/index.js +548 -0
  28. package/members/member-template/runtime/config/local-plugins/shared-asset-injector/openclaw.plugin.json +33 -0
  29. package/members/member-template/runtime/config/local-plugins/shared-asset-injector/package.json +10 -0
  30. package/members/member-template/runtime/config/openclaw.json +104 -0
  31. package/members/member-template/runtime/docker-compose.yml +53 -0
  32. package/members/member-template/runtime/logs/README.md +3 -0
  33. package/members/member-template/runtime/secrets/.gitkeep +1 -0
  34. package/members/member-template/runtime/secrets/README.md +3 -0
  35. package/members/member-template/runtime/workspace/.gitkeep +1 -0
  36. package/members/member-template/runtime/workspace/README.md +3 -0
  37. package/package.json +57 -0
  38. package/pic/banner.jpg +0 -0
  39. package/provision-member.md +87 -0
  40. package/scripts/shared-asset-stack-test.mjs +369 -0
  41. package/scripts/shared-skill-combo-test.mjs +282 -0
  42. package/scripts/team-load-test.mjs +358 -0
  43. package/scripts/verify-install.mjs +44 -0
  44. package/services/litellm/config.yaml +35 -0
  45. package/services/litellm/docker-compose.yml +36 -0
  46. package/services/litellm/litellm.env.example +13 -0
  47. package/shared-snapshots/README.md +16 -0
  48. package/shared-snapshots/docs/README.md +3 -0
  49. package/shared-snapshots/memory/README.md +3 -0
  50. package/shared-snapshots/skills/README.md +3 -0
  51. package/shared-snapshots/tools/README.md +4 -0
  52. package/shared-snapshots/workflows/README.md +3 -0
  53. package/team-assets/README.md +11 -0
  54. package/team-assets/policies/README.md +7 -0
  55. package/team-assets/policies/asset-visibility.md +24 -0
  56. package/team-assets/policies/high-risk-action-approval.md +18 -0
  57. package/team-assets/policies/promotion-rules.md +25 -0
  58. package/team-assets/policies/tool-binding-rules.md +26 -0
  59. package/team-assets/shared-docs/README.md +3 -0
  60. package/team-assets/shared-memory/README.md +8 -0
  61. package/team-assets/shared-skills/README.md +8 -0
  62. package/team-assets/shared-tools/README.md +8 -0
  63. package/team-assets/shared-workflows/README.md +9 -0
@@ -0,0 +1,53 @@
1
+ name: REPLACE_COMPOSE_PROJECT
2
+
3
+ services:
4
+ openclaw-member:
5
+ image: ghcr.io/openclaw/openclaw:latest
6
+ container_name: REPLACE_CONTAINER_ID
7
+ restart: unless-stopped
8
+ ports:
9
+ - "127.0.0.1:18800:18789"
10
+ environment:
11
+ OPENCLAW_CONFIG_DIR: /home/node/.openclaw
12
+ OPENCLAW_WORKSPACE_DIR: /home/node/.openclaw/workspace
13
+ HTTP_PROXY: http://host.docker.internal:17892
14
+ HTTPS_PROXY: http://host.docker.internal:17892
15
+ http_proxy: http://host.docker.internal:17892
16
+ https_proxy: http://host.docker.internal:17892
17
+ NO_PROXY: 127.0.0.1,localhost,::1,host.docker.internal
18
+ no_proxy: 127.0.0.1,localhost,::1,host.docker.internal
19
+ user: "1000:1000"
20
+ extra_hosts:
21
+ - "host.docker.internal:host-gateway"
22
+ security_opt:
23
+ - no-new-privileges:true
24
+ cap_drop:
25
+ - ALL
26
+ read_only: true
27
+ tmpfs:
28
+ - /tmp:size=256m,mode=1777
29
+ pids_limit: 256
30
+ mem_limit: 2g
31
+ cpus: 1.5
32
+ volumes:
33
+ - ./config:/home/node/.openclaw:rw
34
+ - ./workspace:/home/node/.openclaw/workspace:rw
35
+ - ./secrets:/home/node/.openclaw/secrets:rw
36
+ - ../../private-memory:/home/node/.openclaw/workspace/private-memory:rw
37
+ - ../../private-skills:/home/node/.openclaw/workspace/private-skills:rw
38
+ - ../../private-tools:/home/node/.openclaw/workspace/private-tools:rw
39
+ - ../../private-docs:/home/node/.openclaw/workspace/private-docs:rw
40
+ - ../../../../teams/REPLACE_TEAM_SLUG/assets/current:/srv/team-shared:ro
41
+ - ../../../../teams/REPLACE_TEAM_SLUG/assets/current:/home/node/.openclaw/workspace/team-shared:ro
42
+ healthcheck:
43
+ test: ["CMD", "curl", "-fsS", "http://127.0.0.1:18789/healthz"]
44
+ interval: 30s
45
+ timeout: 5s
46
+ retries: 5
47
+ start_period: 30s
48
+ networks:
49
+ - member_isolated
50
+
51
+ networks:
52
+ member_isolated:
53
+ driver: bridge
@@ -0,0 +1,3 @@
1
+ # Runtime logs directory
2
+
3
+ Store local runtime logs here if needed.
@@ -0,0 +1,3 @@
1
+ # Runtime secrets directory
2
+
3
+ Place only this member runtime's secrets here.
@@ -0,0 +1,3 @@
1
+ # Runtime workspace directory
2
+
3
+ This is the isolated runtime workspace for the member container.
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "velaclaw-dev",
3
+ "version": "0.2.0",
4
+ "description": "Velaclaw control-plane MVP prototype",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "velaclaw": "dist/cli.js",
9
+ "velaclaw-dev": "dist/cli.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "scripts",
14
+ "members/member-template",
15
+ "members/README.md",
16
+ "members/LAUNCH_CHECKLIST.md",
17
+ "services/litellm/docker-compose.yml",
18
+ "services/litellm/config.yaml",
19
+ "services/litellm/litellm.env.example",
20
+ "team-assets",
21
+ "shared-snapshots",
22
+ "ARCHITECTURE.md",
23
+ "README.md",
24
+ "README.dev.md",
25
+ "README.public.md",
26
+ "TESTING.md",
27
+ "RELEASING.md",
28
+ "provision-member.md",
29
+ "pic",
30
+ ".gitignore"
31
+ ],
32
+ "engines": {
33
+ "node": ">=22"
34
+ },
35
+ "scripts": {
36
+ "dev": "tsx watch src/server.ts",
37
+ "build": "tsc -p tsconfig.json",
38
+ "start": "node dist/server.js",
39
+ "load:test-team": "node scripts/team-load-test.mjs",
40
+ "smoke:skills": "node scripts/shared-skill-combo-test.mjs",
41
+ "smoke:assets": "node scripts/shared-asset-stack-test.mjs",
42
+ "verify:install": "node scripts/verify-install.mjs",
43
+ "release:check": "npm run build && npm run verify:install && npm pack --dry-run",
44
+ "publish:npm": "npm publish --access public",
45
+ "publish:private": "npm publish",
46
+ "prepack": "npm run build"
47
+ },
48
+ "dependencies": {
49
+ "express": "^4.21.2"
50
+ },
51
+ "devDependencies": {
52
+ "@types/express": "^5.0.1",
53
+ "@types/node": "^22.15.3",
54
+ "tsx": "^4.19.4",
55
+ "typescript": "^5.8.3"
56
+ }
57
+ }
package/pic/banner.jpg ADDED
Binary file
@@ -0,0 +1,87 @@
1
+ # Provision a new Velaclaw member runtime
2
+
3
+ This guide provisions a new member runtime from `members/member-template/`.
4
+
5
+ ## Overview
6
+
7
+ Each member gets:
8
+ - private memory
9
+ - private skills
10
+ - private tool bindings
11
+ - private docs
12
+ - isolated OpenClaw runtime config/workspace/secrets/logs
13
+
14
+ ## Steps
15
+
16
+ ### 1. Copy the template
17
+
18
+ From `velaclaw/members/`:
19
+
20
+ ```bash
21
+ cp -a member-template <member-id>
22
+ ```
23
+
24
+ ### 2. Edit runtime config
25
+
26
+ File:
27
+ - `members/<member-id>/runtime/config/openclaw.json`
28
+
29
+ Replace:
30
+ - `REPLACE_WITH_RANDOM_GATEWAY_TOKEN`
31
+ - `REPLACE_WITH_USER_ID`
32
+
33
+ Optional:
34
+ - assistant identity name
35
+ - model defaults
36
+ - disabled tools
37
+
38
+ ### 3. Add member channel secrets
39
+
40
+ Place secrets in:
41
+ - `members/<member-id>/runtime/secrets/`
42
+
43
+ Examples:
44
+ - `telegram-bot-token`
45
+
46
+ ### 4. Adjust compose settings
47
+
48
+ File:
49
+ - `members/<member-id>/runtime/docker-compose.yml`
50
+
51
+ Replace:
52
+ - `REPLACE_MEMBER_ID`
53
+ - published port `18800` if another member already uses it
54
+
55
+ ### 5. Start runtime
56
+
57
+ From the member runtime directory:
58
+
59
+ ```bash
60
+ docker compose up -d
61
+ ```
62
+
63
+ ### 6. Verify isolation
64
+
65
+ Check:
66
+ - container is healthy
67
+ - only the member's own workspace is mounted
68
+ - no docker socket is mounted
69
+ - member secrets stay local to that runtime
70
+ - high-risk tools remain disabled or approval-gated
71
+
72
+ ## Naming recommendations
73
+
74
+ Use stable member IDs such as:
75
+ - `zane`
76
+ - `alice`
77
+ - `research`
78
+ - `ops`
79
+
80
+ ## Safety checklist
81
+
82
+ - unique bot token per member runtime
83
+ - unique allowlist user ID(s)
84
+ - unique published port if exposing locally
85
+ - no host primary workspace mounts
86
+ - no host docker socket mounts
87
+ - no shared writable team asset mounts
@@ -0,0 +1,369 @@
1
+ import { execFile } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { fileURLToPath } from "node:url";
6
+ import {
7
+ acceptInvitationByCode,
8
+ createInvitationForTeam,
9
+ createTeam,
10
+ createTeamAssetProposal,
11
+ getTeamAssetCapabilityRegistryBySlug,
12
+ getTeamAssetServerManifestBySlug,
13
+ getTeamMemberWorkspaceById,
14
+ runMemberRuntimeActionForTeam
15
+ } from "../dist/data.js";
16
+
17
+ const execFileAsync = promisify(execFile);
18
+ const ROOT = process.env.VELACLAW_ROOT
19
+ ? path.resolve(process.env.VELACLAW_ROOT)
20
+ : path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
21
+
22
+ async function run(command, args, options = {}) {
23
+ const result = await execFileAsync(command, args, {
24
+ cwd: options.cwd || ROOT,
25
+ maxBuffer: 1024 * 1024 * 24
26
+ });
27
+ return {
28
+ stdout: String(result.stdout || ""),
29
+ stderr: String(result.stderr || "")
30
+ };
31
+ }
32
+
33
+ async function ensureReadTool(configPath) {
34
+ const raw = JSON.parse(await fs.readFile(configPath, "utf8"));
35
+ raw.tools ??= {};
36
+ raw.tools.alsoAllow = Array.from(new Set([...(Array.isArray(raw.tools.alsoAllow) ? raw.tools.alsoAllow : []), "read"]));
37
+ raw.tools.fs = {
38
+ ...(typeof raw.tools.fs === "object" && raw.tools.fs ? raw.tools.fs : {}),
39
+ workspaceOnly: true
40
+ };
41
+ await fs.writeFile(configPath, `${JSON.stringify(raw, null, 2)}\n`, "utf8");
42
+ }
43
+
44
+ async function waitForRuntimeHealthy(teamSlug, memberId, timeoutMs = 120000) {
45
+ const started = Date.now();
46
+ while (Date.now() - started < timeoutMs) {
47
+ const { stdout } = await run("curl", ["-sS", `http://127.0.0.1:4318/api/teams/${teamSlug}/members/${memberId}/runtime`]);
48
+ const payload = JSON.parse(stdout);
49
+ if (payload?.runtime?.container?.health === "healthy") {
50
+ return payload.runtime;
51
+ }
52
+ await new Promise((resolve) => setTimeout(resolve, 2000));
53
+ }
54
+ throw new Error(`runtime did not become healthy for ${memberId}`);
55
+ }
56
+
57
+ async function findSessionEntry(sessionsPath, suffix, timeoutMs = 30000) {
58
+ const started = Date.now();
59
+ while (Date.now() - started < timeoutMs) {
60
+ const raw = JSON.parse(await fs.readFile(sessionsPath, "utf8"));
61
+ const key = Object.keys(raw).find((entry) => entry.includes(suffix));
62
+ if (key) {
63
+ return {
64
+ sessionKey: key,
65
+ sessionId: raw[key].sessionId,
66
+ entry: raw[key]
67
+ };
68
+ }
69
+ await new Promise((resolve) => setTimeout(resolve, 1000));
70
+ }
71
+ throw new Error(`session not found for ${suffix}`);
72
+ }
73
+
74
+ function buildSyntheticAssets() {
75
+ return {
76
+ skills: [
77
+ {
78
+ title: "Thesis Merge Composer",
79
+ content: `---
80
+ name: thesis-merge-composer
81
+ description: Merge multiple short drafts into one coherent final memo. Use when the user provides two or more fragments and wants one integrated output.
82
+ ---
83
+
84
+ # Thesis Merge Composer
85
+
86
+ Use this skill when multiple short drafts need to become one coherent memo.
87
+
88
+ Rules:
89
+ - Preserve all meaningful arguments from each draft.
90
+ - Remove repetition.
91
+ - Produce one integrated final memo, not a bullet outline.
92
+ - Prefer natural Chinese prose over formal report boilerplate.
93
+ `
94
+ },
95
+ {
96
+ title: "Consensus Stress Tester",
97
+ content: `---
98
+ name: consensus-stress-tester
99
+ description: Challenge consensus views and identify what the market may already be pricing in. Use for investment or strategic analysis.
100
+ ---
101
+
102
+ # Consensus Stress Tester
103
+
104
+ When a thesis sounds obvious, ask:
105
+ - what is already consensus?
106
+ - what assumptions are embedded in the current narrative?
107
+ - what operational bottlenecks could delay or weaken the expected outcome?
108
+
109
+ Always add a section that stresses the consensus view before giving a final conclusion.
110
+ `
111
+ },
112
+ {
113
+ title: "CN Final Polish",
114
+ content: `---
115
+ name: cn-final-polish
116
+ description: Turn a stiff or AI-sounding Chinese draft into more natural, direct prose.
117
+ ---
118
+
119
+ # CN Final Polish
120
+
121
+ Use this skill for the last pass of Chinese writing.
122
+
123
+ Rules:
124
+ - Remove empty transition phrases.
125
+ - Prefer direct sentences.
126
+ - Avoid exaggerated slogan-like endings.
127
+ - Keep the tone calm, concrete, and readable.
128
+ `
129
+ }
130
+ ],
131
+ memory: {
132
+ title: "Team Preference Ledger",
133
+ content: `# Team Preference Ledger
134
+
135
+ When this workspace writes a final investment memo, include the exact phrase "风险是按时间定价的" once in the final answer.
136
+ `
137
+ },
138
+ workflow: {
139
+ title: "Investment Memo Flow",
140
+ content: `# Investment Memo Flow
141
+
142
+ Follow these three sections in order:
143
+
144
+ 1. Merged View
145
+ 2. Second-Layer Risks
146
+ 3. Final Memo
147
+
148
+ Use these section headings exactly in the final answer.
149
+ `
150
+ }
151
+ };
152
+ }
153
+
154
+ async function createPublishedAssets(teamSlug) {
155
+ const assets = buildSyntheticAssets();
156
+ const created = [];
157
+
158
+ for (const skill of assets.skills) {
159
+ const result = await createTeamAssetProposal({
160
+ teamSlug,
161
+ category: "shared-skills",
162
+ title: skill.title,
163
+ content: skill.content,
164
+ submittedByLabel: "manager",
165
+ sourceZone: "collab"
166
+ });
167
+ created.push({ type: "skill", title: skill.title, assetId: result.asset.id, status: result.asset.status });
168
+ }
169
+
170
+ for (const [type, category, payload] of [
171
+ ["memory", "shared-memory", assets.memory],
172
+ ["workflow", "shared-workflows", assets.workflow]
173
+ ]) {
174
+ const result = await createTeamAssetProposal({
175
+ teamSlug,
176
+ category,
177
+ title: payload.title,
178
+ content: payload.content,
179
+ submittedByLabel: "manager",
180
+ sourceZone: "collab"
181
+ });
182
+ created.push({ type, title: payload.title, assetId: result.asset.id, status: result.asset.status });
183
+ }
184
+
185
+ return created;
186
+ }
187
+
188
+ async function runMemberScenario(containerName, sessionId, prompt) {
189
+ const { stdout } = await run("sudo", [
190
+ "-n",
191
+ "docker",
192
+ "exec",
193
+ containerName,
194
+ "sh",
195
+ "-lc",
196
+ `cd /app && openclaw agent --session-id ${sessionId} --message ${JSON.stringify(prompt)} --json`
197
+ ]);
198
+ return JSON.parse(stdout);
199
+ }
200
+
201
+ async function readSessionArtifacts(memberRoot, suffix) {
202
+ const sessionsPath = path.join(memberRoot, "config", "agents", "main", "sessions", "sessions.json");
203
+ const { sessionId, sessionKey, entry } = await findSessionEntry(sessionsPath, suffix);
204
+ const jsonlPath = path.join(memberRoot, "config", "agents", "main", "sessions", `${sessionId}.jsonl`);
205
+ const jsonlLines = (await fs.readFile(jsonlPath, "utf8"))
206
+ .trim()
207
+ .split("\n")
208
+ .filter(Boolean)
209
+ .map((line) => JSON.parse(line));
210
+
211
+ const assistantToolCalls = jsonlLines
212
+ .filter((item) => item.type === "message" && item.message?.role === "assistant")
213
+ .flatMap((item) => item.message.content || [])
214
+ .filter((item) => item.type === "toolCall")
215
+ .map((item) => ({
216
+ name: item.name,
217
+ arguments: item.arguments
218
+ }));
219
+
220
+ const finalAssistantText =
221
+ jsonlLines
222
+ .filter((item) => item.type === "message" && item.message?.role === "assistant")
223
+ .flatMap((item) => item.message.content || [])
224
+ .filter((item) => item.type === "text")
225
+ .map((item) => item.text)
226
+ .join("\n\n") || "";
227
+
228
+ return {
229
+ sessionId,
230
+ sessionKey,
231
+ entry,
232
+ assistantToolCalls,
233
+ finalAssistantText,
234
+ jsonlPath
235
+ };
236
+ }
237
+
238
+ async function main() {
239
+ await fs.mkdir(path.join(ROOT, "artifacts"), { recursive: true });
240
+ const teamSuffix = Date.now();
241
+ const team = await createTeam({
242
+ name: `Asset Stack Test ${teamSuffix}`,
243
+ slug: `asset-stack-test-${teamSuffix}`,
244
+ description: "Synthetic skills/memory/workflow combination regression test",
245
+ managerLabel: "Test Manager"
246
+ });
247
+
248
+ const createdAssets = await createPublishedAssets(team.slug);
249
+ const invitation = await createInvitationForTeam(team.slug, {
250
+ inviteeLabel: "Asset Stack Probe",
251
+ memberId: "asset-stack-probe@team.local",
252
+ memberEmail: "asset-stack-probe@team.local",
253
+ role: "member",
254
+ createdBy: "shared-asset-stack-test"
255
+ });
256
+ const accepted = await acceptInvitationByCode(invitation.code, {
257
+ identityName: "Asset Stack Probe"
258
+ });
259
+
260
+ const memberId = accepted.policy.memberId;
261
+ const memberRoot = path.join(ROOT, "members", team.slug, memberId, "runtime");
262
+ const configPath = path.join(memberRoot, "config", "openclaw.json");
263
+ await ensureReadTool(configPath);
264
+ await runMemberRuntimeActionForTeam(team.slug, memberId, "start");
265
+ const runtime = await waitForRuntimeHealthy(team.slug, memberId);
266
+
267
+ const probeWorkspace = await getTeamMemberWorkspaceById(team.slug, memberId);
268
+ const registry = await getTeamAssetCapabilityRegistryBySlug(team.slug);
269
+ const workspaceSkillFiles = (await fs.readdir(path.join(memberRoot, "workspace", "skills"))).sort();
270
+
271
+ const skillMemoryPrompt = [
272
+ "请整合下面两段投资草稿,先挑战市场共识,再给我一版自然的中文最终稿。",
273
+ "",
274
+ "草稿A:市场普遍认为该公司会因为 AI 数据中心扩张而快速增长。",
275
+ "草稿B:审批、资本开支和供应链可能让业绩兑现更慢。",
276
+ "",
277
+ "如果团队有固定写作偏好,请遵守。"
278
+ ].join("\n");
279
+
280
+ const workflowPrompt = [
281
+ "请按团队共享流程输出一版投资分析。",
282
+ "",
283
+ "输入观点:公司受益于 AI 数据中心电力需求,但兑现节奏可能慢于市场预期。",
284
+ "",
285
+ "请严格按共享流程输出最终结果。"
286
+ ].join("\n");
287
+
288
+ const skillMemoryResult = await runMemberScenario(runtime.containerName, "asset-stack-skill-memory", skillMemoryPrompt);
289
+ const workflowResult = await runMemberScenario(runtime.containerName, "asset-stack-workflow", workflowPrompt);
290
+
291
+ const skillMemorySession = await readSessionArtifacts(memberRoot, "asset-stack-skill-memory");
292
+ const workflowSession = await readSessionArtifacts(memberRoot, "asset-stack-workflow");
293
+
294
+ const report = {
295
+ createdAt: new Date().toISOString(),
296
+ team,
297
+ member: {
298
+ memberId,
299
+ runtime,
300
+ workspaceSkillFiles
301
+ },
302
+ createdAssets,
303
+ registryCounts: registry.counts,
304
+ registryTitles: Object.fromEntries(
305
+ Object.entries(registry.byKind).map(([kind, items]) => [kind, items.map((item) => item.title)])
306
+ ),
307
+ scenarios: {
308
+ skillMemory: {
309
+ sessionId: skillMemorySession.sessionId,
310
+ loadedSkills: (skillMemorySession.entry.skillsSnapshot?.skills || []).map((entry) => entry.name),
311
+ toolCalls: skillMemorySession.assistantToolCalls,
312
+ finalAssistantText:
313
+ skillMemoryResult?.result?.payloads?.map((entry) => entry.text).filter(Boolean).join("\n\n") ||
314
+ skillMemorySession.finalAssistantText
315
+ },
316
+ workflow: {
317
+ sessionId: workflowSession.sessionId,
318
+ loadedSkills: (workflowSession.entry.skillsSnapshot?.skills || []).map((entry) => entry.name),
319
+ toolCalls: workflowSession.assistantToolCalls,
320
+ finalAssistantText:
321
+ workflowResult?.result?.payloads?.map((entry) => entry.text).filter(Boolean).join("\n\n") ||
322
+ workflowSession.finalAssistantText
323
+ }
324
+ },
325
+ checks: {
326
+ sharedSkillsDiscovered:
327
+ workspaceSkillFiles.filter((name) => name.startsWith("team-shared-")).length >= 3,
328
+ gatewayUsedForRuns:
329
+ skillMemorySession.entry.modelProvider === "team-gateway" &&
330
+ workflowSession.entry.modelProvider === "team-gateway",
331
+ skillMemoryReadsSharedSkills:
332
+ skillMemorySession.assistantToolCalls.some((call) => call.name === "read" && String(call.arguments?.path || "").includes("team-shared-")),
333
+ skillMemoryReadsSharedMemory:
334
+ skillMemorySession.assistantToolCalls.some((call) => {
335
+ const targetPath = String(call.arguments?.path || "");
336
+ return call.name === "read" && (
337
+ targetPath.includes("/MEMORY.md") ||
338
+ targetPath.includes("/docs/team-shared/active/memory/") ||
339
+ targetPath.includes("team-shared-memory-")
340
+ );
341
+ }) ||
342
+ Array.isArray(skillMemorySession.entry.systemPromptReport?.injectedWorkspaceFiles) &&
343
+ skillMemorySession.entry.systemPromptReport.injectedWorkspaceFiles.some((file) => file?.name === "MEMORY.md" && file?.missing === false),
344
+ skillMemoryOutputReflectsMemory:
345
+ (skillMemoryResult?.result?.payloads?.map((entry) => entry.text).join("\n") || "").includes("风险是按时间定价的"),
346
+ workflowReadsSharedWorkflow:
347
+ workflowSession.assistantToolCalls.some((call) => {
348
+ const targetPath = String(call.arguments?.path || "");
349
+ return call.name === "read" && (
350
+ targetPath.includes("/docs/team-shared/active/workflows/") ||
351
+ targetPath.includes("team-shared-workflow-")
352
+ );
353
+ }),
354
+ workflowOutputUsesExpectedSections:
355
+ ["Merged View", "Second-Layer Risks", "Final Memo"].every((heading) =>
356
+ (workflowResult?.result?.payloads?.map((entry) => entry.text).join("\n") || "").includes(heading)
357
+ )
358
+ }
359
+ };
360
+
361
+ const artifactPath = path.join(ROOT, "artifacts", `shared-asset-stack-test-${teamSuffix}.json`);
362
+ await fs.writeFile(artifactPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
363
+ console.log(JSON.stringify({ artifactPath, report }, null, 2));
364
+ }
365
+
366
+ main().catch((error) => {
367
+ console.error(error instanceof Error ? error.stack || error.message : String(error));
368
+ process.exitCode = 1;
369
+ });