dot-studio 0.0.1 → 0.0.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 (148) hide show
  1. package/README.md +20 -200
  2. package/client/assets/ActFrame-BYOBkLYW.js +1 -0
  3. package/client/assets/ActFrame-C_WEt6bv.css +1 -0
  4. package/client/assets/ActInspectorPanel-C3VlS7tB.js +1 -0
  5. package/client/assets/ActInspectorPanel-CE6s6GYv.css +1 -0
  6. package/client/assets/AssistantChat-BOyW0K79.js +1 -0
  7. package/client/assets/AssistantChat-DoVmHvMJ.css +1 -0
  8. package/client/assets/CanvasTerminalFrame-BC-79q9U.css +1 -0
  9. package/client/assets/CanvasTerminalFrame-DxKbexK6.js +4 -0
  10. package/client/assets/CanvasTrackingFrame-DumxhNwg.js +1 -0
  11. package/client/assets/CanvasTrackingFrame-G4rRrfne.css +1 -0
  12. package/client/assets/CanvasWindowFrame-ziJeVfHG.js +1 -0
  13. package/client/assets/DanceBundleEditorFrame-CH8VDUMK.js +1 -0
  14. package/client/assets/DanceBundleEditorFrame-DaLqMflT.css +1 -0
  15. package/client/assets/MarkdownEditorFrame-DVecIZpZ.css +1 -0
  16. package/client/assets/MarkdownEditorFrame-Dwpgs2GX.js +2 -0
  17. package/client/assets/MarkdownRenderer-Cz8A4AgP.js +1 -0
  18. package/client/assets/PublishModal-DUlHz0fT.js +1 -0
  19. package/client/assets/TodoDock-DcVf7zQG.js +1 -0
  20. package/client/assets/WorkspaceToolbar-CXYi_sMD.js +2 -0
  21. package/client/assets/WorkspaceToolbar-CiQvVocC.css +1 -0
  22. package/client/assets/chat-message-visibility-YwJ-AQno.js +11 -0
  23. package/client/assets/dnd-vendor-CIAZE2P2.js +5 -0
  24. package/client/assets/flow-vendor-BZV40eAE.css +1 -0
  25. package/client/assets/flow-vendor-C868rU-6.js +23 -0
  26. package/client/assets/icon-vendor-I2JVIi1s.js +501 -0
  27. package/client/assets/index-BMY4hrBP.js +3 -0
  28. package/client/assets/index-C-vnj9y3.js +1 -0
  29. package/client/assets/index-C9HTqfZw.css +1 -0
  30. package/client/assets/index-CWrv6O3o.js +64 -0
  31. package/client/assets/index-DMS12-Q2.js +8 -0
  32. package/client/assets/index-Dn7t_Y7G.js +1 -0
  33. package/client/assets/index-p-wk7iGH.css +1 -0
  34. package/client/assets/markdown-vendor-BSTcku12.css +10 -0
  35. package/client/assets/markdown-vendor-DnTJ9hmR.js +35 -0
  36. package/client/assets/participant-labels-Cf3qP3GB.js +1 -0
  37. package/client/assets/queries-Dm1jEHfc.js +1 -0
  38. package/client/assets/query-vendor-_taqgrbn.js +1 -0
  39. package/client/assets/react-vendor-DzpMUNDT.js +49 -0
  40. package/client/assets/settings-utils-l7KCS3Ev.js +1 -0
  41. package/client/assets/terminal-vendor-6GBZ9nXN.css +32 -0
  42. package/client/assets/terminal-vendor-D0xRnmbI.js +112 -0
  43. package/client/index.html +13 -3
  44. package/dist/cli.js +25 -3
  45. package/dist/server/app.js +72 -0
  46. package/dist/server/index.js +2 -62
  47. package/dist/server/lib/act-session-policy.js +31 -0
  48. package/dist/server/lib/chat-session.js +101 -0
  49. package/dist/server/lib/config.js +18 -4
  50. package/dist/server/lib/dot-authoring.js +171 -102
  51. package/dist/server/lib/dot-loader.js +9 -8
  52. package/dist/server/lib/dot-login.js +8 -190
  53. package/dist/server/lib/dot-source.js +11 -0
  54. package/dist/server/lib/model-catalog.js +74 -15
  55. package/dist/server/lib/opencode-auth.js +4 -1
  56. package/dist/server/lib/opencode-errors.js +70 -38
  57. package/dist/server/lib/opencode-sidecar.js +5 -2
  58. package/dist/server/lib/project-config.js +8 -0
  59. package/dist/server/lib/runtime-tools.js +46 -8
  60. package/dist/server/lib/safe-mode.js +410 -0
  61. package/dist/server/lib/session-execution.js +81 -0
  62. package/dist/server/lib/sse.js +22 -0
  63. package/dist/server/routes/act-runtime-threads.js +156 -0
  64. package/dist/server/routes/act-runtime-tools.js +157 -0
  65. package/dist/server/routes/act-runtime.js +7 -0
  66. package/dist/server/routes/adapter.js +32 -0
  67. package/dist/server/routes/assets-collection.js +16 -0
  68. package/dist/server/routes/assets-detail.js +38 -0
  69. package/dist/server/routes/assets.js +4 -158
  70. package/dist/server/routes/chat-messages.js +104 -0
  71. package/dist/server/routes/chat-sessions.js +104 -0
  72. package/dist/server/routes/chat-stream.js +15 -0
  73. package/dist/server/routes/chat.js +6 -353
  74. package/dist/server/routes/compile.js +5 -91
  75. package/dist/server/routes/dot-assets.js +77 -0
  76. package/dist/server/routes/dot-core.js +62 -0
  77. package/dist/server/routes/dot-performer.js +80 -0
  78. package/dist/server/routes/dot.js +6 -267
  79. package/dist/server/routes/drafts-collection.js +40 -0
  80. package/dist/server/routes/drafts-dance-bundle.js +113 -0
  81. package/dist/server/routes/drafts-item.js +86 -0
  82. package/dist/server/routes/drafts.js +9 -0
  83. package/dist/server/routes/health.js +18 -33
  84. package/dist/server/routes/opencode-core.js +120 -0
  85. package/dist/server/routes/opencode-file.js +67 -0
  86. package/dist/server/routes/opencode-mcp.js +74 -0
  87. package/dist/server/routes/opencode-provider.js +41 -0
  88. package/dist/server/routes/opencode.js +8 -418
  89. package/dist/server/routes/route-errors.js +10 -0
  90. package/dist/server/routes/safe-actions.js +60 -0
  91. package/dist/server/routes/safe-summary.js +20 -0
  92. package/dist/server/routes/safe.js +7 -0
  93. package/dist/server/routes/workspaces.js +47 -0
  94. package/dist/server/services/act-runtime/act-context-builder.js +81 -0
  95. package/dist/server/services/act-runtime/act-runtime-service.js +313 -0
  96. package/dist/server/services/act-runtime/act-runtime-utils.js +10 -0
  97. package/dist/server/services/act-runtime/act-tool-projection.js +26 -0
  98. package/dist/server/services/act-runtime/act-tools.js +151 -0
  99. package/dist/server/services/act-runtime/board-persistence.js +38 -0
  100. package/dist/server/services/act-runtime/event-logger.js +73 -0
  101. package/dist/server/services/act-runtime/event-router.js +102 -0
  102. package/dist/server/services/act-runtime/mailbox.js +149 -0
  103. package/dist/server/services/act-runtime/safety-guard.js +162 -0
  104. package/dist/server/services/act-runtime/session-queue.js +114 -0
  105. package/dist/server/services/act-runtime/thread-manager.js +351 -0
  106. package/dist/server/services/act-runtime/wake-cascade.js +306 -0
  107. package/dist/server/services/act-runtime/wake-evaluator.js +43 -0
  108. package/dist/server/services/act-runtime/wake-performer-resolver.js +68 -0
  109. package/dist/server/services/act-runtime/wake-prompt-builder.js +77 -0
  110. package/dist/server/services/adapter-view-service.js +6 -0
  111. package/dist/server/services/asset-service.js +366 -0
  112. package/dist/server/services/chat-event-stream-service.js +157 -0
  113. package/dist/server/services/chat-service.js +207 -0
  114. package/dist/server/services/chat-session-service.js +203 -0
  115. package/dist/server/services/compile-service.js +4 -0
  116. package/dist/server/services/dance-bundle-service.js +222 -0
  117. package/dist/server/services/dot-add-service.js +59 -0
  118. package/dist/server/services/dot-service.js +178 -0
  119. package/dist/server/services/draft-service.js +367 -0
  120. package/dist/server/services/opencode-projection/dance-compiler.js +164 -0
  121. package/dist/server/services/opencode-projection/performer-compiler.js +195 -0
  122. package/dist/server/services/opencode-projection/preview-service.js +31 -0
  123. package/dist/server/services/opencode-projection/projection-manifest.js +98 -0
  124. package/dist/server/services/opencode-projection/stage-projection-service.js +188 -0
  125. package/dist/server/services/opencode-service.js +338 -0
  126. package/dist/server/services/safe-service.js +33 -0
  127. package/dist/server/services/studio-assistant/assistant-service.js +172 -0
  128. package/dist/server/services/studio-service.js +69 -0
  129. package/dist/server/services/workspace-service.js +224 -0
  130. package/dist/server/terminal.js +57 -11
  131. package/dist/shared/act-types.js +4 -0
  132. package/dist/shared/adapter-view.js +1 -0
  133. package/dist/shared/asset-contracts.js +1 -0
  134. package/dist/shared/assistant-actions.js +1 -0
  135. package/dist/shared/chat-contracts.js +1 -0
  136. package/dist/shared/dot-contracts.js +1 -0
  137. package/dist/shared/dot-types.js +4 -0
  138. package/dist/shared/draft-contracts.js +2 -0
  139. package/dist/shared/model-types.js +2 -0
  140. package/dist/shared/performer-mcp-portability.js +10 -0
  141. package/dist/shared/safe-mode.js +1 -0
  142. package/dist/shared/session-metadata.js +4 -3
  143. package/package.json +6 -4
  144. package/client/assets/index-C2eIILoa.css +0 -41
  145. package/client/assets/index-DUPZ_Lw5.js +0 -616
  146. package/dist/server/lib/act-runtime.js +0 -1282
  147. package/dist/server/lib/prompt.js +0 -222
  148. package/dist/server/routes/stages.js +0 -137
@@ -0,0 +1,195 @@
1
+ import { createHash } from 'crypto';
2
+ import { getAssetPayload } from '../../lib/dot-source.js';
3
+ import { resolveRuntimeModel } from '../../lib/model-catalog.js';
4
+ import { findRuntimeModelVariant } from '../../../shared/model-variants.js';
5
+ import { toRelativePath, resolveAgentIdentity } from './projection-manifest.js';
6
+ import { readDraftTextContent } from '../draft-service.js';
7
+ async function resolveTalContent(cwd, ref) {
8
+ if (!ref) {
9
+ return null;
10
+ }
11
+ if (ref.kind === 'registry') {
12
+ return getAssetPayload(cwd, ref.urn);
13
+ }
14
+ return readDraftTextContent(cwd, 'tal', ref.draftId);
15
+ }
16
+ function buildTalSection(talContent) {
17
+ if (!talContent) {
18
+ return [
19
+ '# Core Instructions',
20
+ 'No core instruction asset is configured. Follow the user request directly and stay consistent with the current session context.',
21
+ ].join('\n');
22
+ }
23
+ return [
24
+ '# Core Instructions',
25
+ '',
26
+ talContent,
27
+ ].join('\n');
28
+ }
29
+ function buildBody(input) {
30
+ return [
31
+ buildTalSection(input.talContent),
32
+ input.collaborationPromptSection || null,
33
+ input.relationPromptSection || null,
34
+ ].filter(Boolean).join('\n\n');
35
+ }
36
+ function buildSkillPermissionLines(skillNames) {
37
+ const lines = ['permission:', ' skill:', ' "*": "deny"'];
38
+ for (const skillName of skillNames) {
39
+ lines.push(` ${JSON.stringify(skillName)}: "allow"`);
40
+ }
41
+ return lines;
42
+ }
43
+ function buildTaskPermissionLines(taskAllowlist) {
44
+ if (taskAllowlist.length === 0) {
45
+ return [];
46
+ }
47
+ const lines = [' task:', ' "*": "deny"'];
48
+ for (const agentName of taskAllowlist) {
49
+ lines.push(` ${JSON.stringify(agentName)}: "allow"`);
50
+ }
51
+ return lines;
52
+ }
53
+ function buildToolsLines(toolMap, posture) {
54
+ const pairs = Object.entries(toolMap).sort(([left], [right]) => left.localeCompare(right));
55
+ if (posture === 'plan') {
56
+ pairs.push(['bash', false], ['edit', false], ['write', false]);
57
+ }
58
+ if (pairs.length === 0) {
59
+ return [];
60
+ }
61
+ const lines = ['tools:'];
62
+ for (const [tool, enabled] of pairs) {
63
+ lines.push(` ${JSON.stringify(tool)}: ${enabled ? 'true' : 'false'}`);
64
+ }
65
+ return lines;
66
+ }
67
+ function buildFrontmatter(input) {
68
+ const lines = ['---'];
69
+ lines.push(`description: ${JSON.stringify(`Agent: ${input.performerName}`)}`);
70
+ lines.push('mode: primary');
71
+ if (input.model) {
72
+ const modelStr = input.model.modelId.startsWith(`${input.model.provider}/`)
73
+ ? input.model.modelId
74
+ : `${input.model.provider}/${input.model.modelId}`;
75
+ lines.push(`model: ${JSON.stringify(modelStr)}`);
76
+ }
77
+ if (input.variantId) {
78
+ lines.push(`variant: ${JSON.stringify(input.variantId)}`);
79
+ }
80
+ lines.push(...buildSkillPermissionLines(input.skillNames));
81
+ lines.push(...buildTaskPermissionLines(input.taskAllowlist || []));
82
+ lines.push(...buildToolsLines(input.toolMap, input.posture));
83
+ lines.push('---');
84
+ return lines.join('\n');
85
+ }
86
+ function buildAgentFile(input) {
87
+ const identity = resolveAgentIdentity({
88
+ executionDir: input.executionDir,
89
+ workspaceHash: input.workspaceHash,
90
+ performerId: input.performerId,
91
+ posture: input.posture,
92
+ scope: input.scope,
93
+ actId: input.actId,
94
+ });
95
+ const frontmatter = buildFrontmatter({
96
+ performerName: input.performerName,
97
+ model: input.model,
98
+ posture: input.posture,
99
+ variantId: input.variantId,
100
+ skillNames: input.skillNames,
101
+ toolMap: input.toolMap,
102
+ taskAllowlist: input.taskAllowlist,
103
+ });
104
+ const content = `${frontmatter}\n\n${input.body}`;
105
+ return {
106
+ agentName: identity.agentName,
107
+ filePath: identity.filePath,
108
+ relativePath: toRelativePath(input.executionDir, identity.filePath),
109
+ content,
110
+ };
111
+ }
112
+ export async function compilePerformer(cwd, input, skills) {
113
+ const talContent = await resolveTalContent(cwd, input.talRef);
114
+ let resolvedVariantId = null;
115
+ if (input.model) {
116
+ const runtimeModel = await resolveRuntimeModel(cwd, input.model);
117
+ if (runtimeModel) {
118
+ const selectedVariant = findRuntimeModelVariant([runtimeModel], input.model.provider, input.model.modelId, input.modelVariant || null);
119
+ resolvedVariantId = selectedVariant?.id || null;
120
+ }
121
+ else {
122
+ resolvedVariantId = input.modelVariant || null;
123
+ }
124
+ }
125
+ const body = buildBody({
126
+ talContent,
127
+ collaborationPromptSection: input.collaborationPromptSection || null,
128
+ relationPromptSection: input.relationPromptSection || null,
129
+ });
130
+ const projectionScope = input.scope === 'stage' ? 'workspace' : (input.scope || 'workspace');
131
+ const buildFile = buildAgentFile({
132
+ workspaceHash: input.workspaceHash,
133
+ performerId: input.performerId,
134
+ performerName: input.performerName,
135
+ executionDir: input.executionDir,
136
+ scope: projectionScope,
137
+ actId: input.actId,
138
+ model: input.model,
139
+ posture: 'build',
140
+ variantId: resolvedVariantId,
141
+ skillNames: input.skillNames,
142
+ toolMap: input.toolMap,
143
+ taskAllowlist: input.taskAllowlist,
144
+ body,
145
+ });
146
+ // Act scope: build-only (no plan agent — complex multi-performer Acts
147
+ // make plan mode impractical to control across the whole graph).
148
+ const includePlan = projectionScope !== 'act';
149
+ const planFile = includePlan
150
+ ? buildAgentFile({
151
+ workspaceHash: input.workspaceHash,
152
+ performerId: input.performerId,
153
+ performerName: input.performerName,
154
+ executionDir: input.executionDir,
155
+ scope: projectionScope,
156
+ actId: input.actId,
157
+ model: input.model,
158
+ posture: 'plan',
159
+ variantId: resolvedVariantId,
160
+ skillNames: input.skillNames,
161
+ toolMap: input.toolMap,
162
+ taskAllowlist: input.taskAllowlist,
163
+ body,
164
+ })
165
+ : null;
166
+ const hashInput = [
167
+ buildFile.content,
168
+ planFile?.content,
169
+ ...skills.map((skill) => skill.content),
170
+ ].filter(Boolean).join('\n\n');
171
+ const projectionHash = createHash('sha256').update(hashInput).digest('hex').slice(0, 16);
172
+ const allFiles = [
173
+ buildFile.relativePath,
174
+ ...(planFile ? [planFile.relativePath] : []),
175
+ ...skills.map((skill) => skill.relativePath),
176
+ ];
177
+ return {
178
+ performerId: input.performerId,
179
+ agentNames: {
180
+ build: buildFile.agentName,
181
+ ...(planFile ? { plan: planFile.agentName } : {}),
182
+ },
183
+ agentPaths: {
184
+ build: buildFile.filePath,
185
+ ...(planFile ? { plan: planFile.filePath } : {}),
186
+ },
187
+ agentContents: {
188
+ build: buildFile.content,
189
+ ...(planFile ? { plan: planFile.content } : {}),
190
+ },
191
+ skills,
192
+ projectionHash,
193
+ allFiles,
194
+ };
195
+ }
@@ -0,0 +1,31 @@
1
+ import { ensurePerformerProjection } from './stage-projection-service.js';
2
+ export function getCompileRequestTargets(request) {
3
+ return request.requestTargets || request.relatedPerformers || [];
4
+ }
5
+ export async function compileProjectionPreview(cwd, request) {
6
+ const posture = request.planMode ? 'plan' : 'build';
7
+ const ensured = await ensurePerformerProjection({
8
+ performerId: request.performerId || 'preview',
9
+ performerName: request.performerName || 'Preview',
10
+ talRef: request.talRef,
11
+ danceRefs: request.danceRefs,
12
+ model: request.model,
13
+ modelVariant: request.modelVariant || null,
14
+ mcpServerNames: request.mcpServerNames || [],
15
+ executionDir: cwd,
16
+ workingDir: cwd,
17
+ requestTargets: getCompileRequestTargets(request),
18
+ });
19
+ return {
20
+ system: ensured.compiled.agentContents[posture],
21
+ agent: ensured.compiled.agentNames[posture],
22
+ danceCatalog: ensured.compiled.skills.map((skill) => ({
23
+ urn: skill.logicalName,
24
+ description: skill.description,
25
+ loadMode: 'tool',
26
+ })),
27
+ deliveryMode: 'tool',
28
+ capabilitySnapshot: ensured.capabilitySnapshot,
29
+ toolResolution: ensured.toolResolution,
30
+ };
31
+ }
@@ -0,0 +1,98 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ const MANIFEST_FILENAME = 'dot-studio.manifest.json';
4
+ const NAMESPACE = 'dot-studio';
5
+ function manifestPath(executionDir) {
6
+ return path.join(executionDir, '.opencode', MANIFEST_FILENAME);
7
+ }
8
+ export async function readManifest(executionDir) {
9
+ try {
10
+ const raw = await fs.readFile(manifestPath(executionDir), 'utf-8');
11
+ return JSON.parse(raw);
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ export async function writeManifest(executionDir, manifest) {
18
+ const filePath = manifestPath(executionDir);
19
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
20
+ await fs.writeFile(filePath, JSON.stringify(manifest, null, 2), 'utf-8');
21
+ }
22
+ export async function cleanGroupFiles(executionDir, groupKey, currentFiles) {
23
+ const existing = await readManifest(executionDir);
24
+ if (!existing) {
25
+ return;
26
+ }
27
+ const currentSet = new Set(currentFiles);
28
+ for (const file of existing.groups[groupKey] || []) {
29
+ if (!currentSet.has(file)) {
30
+ await fs.rm(path.join(executionDir, file), { force: true, recursive: true }).catch(() => { });
31
+ }
32
+ }
33
+ }
34
+ export async function updateManifestGroup(executionDir, workspaceHash, groupKey, files) {
35
+ const current = (await readManifest(executionDir)) || {
36
+ version: 1,
37
+ owner: NAMESPACE,
38
+ workspaceHash,
39
+ groups: {},
40
+ };
41
+ current.workspaceHash = workspaceHash;
42
+ current.groups[groupKey] = files;
43
+ await writeManifest(executionDir, current);
44
+ }
45
+ export async function updateGitExclude(executionDir) {
46
+ const gitDir = path.join(executionDir, '.git');
47
+ const gitStat = await fs.stat(gitDir).catch(() => null);
48
+ if (!gitStat?.isDirectory()) {
49
+ return;
50
+ }
51
+ const excludePath = path.join(gitDir, 'info', 'exclude');
52
+ const marker = '# dot-studio projection (auto-managed)';
53
+ const patterns = [
54
+ marker,
55
+ '.opencode/agents/dot-studio/',
56
+ '.opencode/skills/dot-studio/',
57
+ '.opencode/dot-studio.manifest.json',
58
+ ];
59
+ let content = '';
60
+ try {
61
+ content = await fs.readFile(excludePath, 'utf-8');
62
+ }
63
+ catch {
64
+ // ignore missing exclude file
65
+ }
66
+ if (content.includes(marker)) {
67
+ return;
68
+ }
69
+ await fs.mkdir(path.dirname(excludePath), { recursive: true });
70
+ const separator = content === '' || content.endsWith('\n') ? '' : '\n';
71
+ await fs.writeFile(excludePath, content + separator + patterns.join('\n') + '\n', 'utf-8');
72
+ }
73
+ export function agentProjectionDir(executionDir, workspaceHash, scope = 'workspace', actId) {
74
+ if (scope === 'act' && actId) {
75
+ return path.join(executionDir, '.opencode', 'agents', NAMESPACE, 'act', workspaceHash, actId);
76
+ }
77
+ return path.join(executionDir, '.opencode', 'agents', NAMESPACE, 'workspace', workspaceHash);
78
+ }
79
+ export function localSkillProjectionDir(executionDir, workspaceHash, performerId, scope = 'workspace', actId) {
80
+ if (scope === 'act' && actId) {
81
+ return path.join(executionDir, '.opencode', 'skills', NAMESPACE, 'act', workspaceHash, actId, performerId);
82
+ }
83
+ return path.join(executionDir, '.opencode', 'skills', NAMESPACE, 'workspace', workspaceHash, performerId);
84
+ }
85
+ export function toRelativePath(executionDir, absPath) {
86
+ return path.relative(executionDir, absPath);
87
+ }
88
+ /**
89
+ * Single source of truth for agent identity.
90
+ * agentName is mechanically derived from filePath — never manually assembled.
91
+ */
92
+ export function resolveAgentIdentity(input) {
93
+ const dir = agentProjectionDir(input.executionDir, input.workspaceHash, input.scope, input.actId);
94
+ const fileName = `${input.performerId}--${input.posture}.md`;
95
+ const filePath = path.join(dir, fileName);
96
+ const agentName = path.relative(path.join(input.executionDir, '.opencode', 'agents'), filePath).replace(/\.md$/, '');
97
+ return { agentName, filePath, fileName };
98
+ }
@@ -0,0 +1,188 @@
1
+ import fs from 'fs/promises';
2
+ import { createHash } from 'crypto';
3
+ import path from 'path';
4
+ import { getOpencode } from '../../lib/opencode.js';
5
+ import { resolveRuntimeModel } from '../../lib/model-catalog.js';
6
+ import { resolveRuntimeTools } from '../../lib/runtime-tools.js';
7
+ import { cleanGroupFiles, updateGitExclude, updateManifestGroup, resolveAgentIdentity, } from './projection-manifest.js';
8
+ import { compileDance } from './dance-compiler.js';
9
+ import { compilePerformer } from './performer-compiler.js';
10
+ import { COLLABORATION_TOOL_NAMES, LEGACY_COLLABORATION_TOOL_NAMES } from '../act-runtime/act-tools.js';
11
+ function compileMentionRelations(targets) {
12
+ if (targets.length === 0) {
13
+ return { taskAllowlist: [], promptSection: null };
14
+ }
15
+ const lines = [
16
+ '# Available Agents',
17
+ '',
18
+ 'The following agents are available for @mention in this context.',
19
+ 'Use the `task` tool only when it is actually useful, and only with the allowed agent names below.',
20
+ '',
21
+ ];
22
+ for (const target of targets) {
23
+ lines.push(`- **${target.performerName}**: use \`task\` with agent="${target.agentName}"${target.description ? ` — ${target.description}` : ''}`);
24
+ }
25
+ return {
26
+ taskAllowlist: targets.map((target) => target.agentName),
27
+ promptSection: lines.join('\n'),
28
+ };
29
+ }
30
+ function computeWorkspaceHash(workingDir) {
31
+ return createHash('sha1').update(workingDir).digest('hex').slice(0, 12);
32
+ }
33
+ function groupKey(performerId) {
34
+ return `performer:${performerId}`;
35
+ }
36
+ async function writeIfChanged(filePath, content) {
37
+ const current = await fs.readFile(filePath, 'utf-8').catch(() => null);
38
+ if (current === content) {
39
+ return false;
40
+ }
41
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
42
+ await fs.writeFile(filePath, content, 'utf-8');
43
+ return true;
44
+ }
45
+ async function allMcpToolIds(cwd) {
46
+ const oc = await getOpencode();
47
+ const res = await oc.mcp.status({ directory: cwd });
48
+ const statusMap = ((res && typeof res === 'object' && 'data' in res ? res.data : {}) || {});
49
+ return Array.from(new Set(Object.values(statusMap).flatMap((entry) => (entry?.tools || [])
50
+ .map((tool) => (typeof tool?.name === 'string' ? tool.name : ''))
51
+ .filter((toolId) => toolId.length > 0))));
52
+ }
53
+ function buildProjectedToolMap(allToolIds, resolvedToolIds) {
54
+ const resolved = new Set(resolvedToolIds);
55
+ return Object.fromEntries(allToolIds.map((toolId) => [toolId, resolved.has(toolId)]));
56
+ }
57
+ async function resolveCapabilitySnapshot(cwd, model) {
58
+ if (!model) {
59
+ return null;
60
+ }
61
+ const runtimeModel = await resolveRuntimeModel(cwd, model);
62
+ if (!runtimeModel) {
63
+ return null;
64
+ }
65
+ return {
66
+ toolCall: runtimeModel.toolCall,
67
+ reasoning: runtimeModel.reasoning,
68
+ attachment: runtimeModel.attachment,
69
+ temperature: runtimeModel.temperature,
70
+ modalities: runtimeModel.modalities,
71
+ };
72
+ }
73
+ export async function ensurePerformerProjection(input) {
74
+ const workspaceHash = computeWorkspaceHash(input.workingDir);
75
+ const toolResolution = await resolveRuntimeTools(input.executionDir, input.model, input.mcpServerNames);
76
+ const toolMap = buildProjectedToolMap(await allMcpToolIds(input.executionDir), toolResolution.resolvedTools);
77
+ if (input.extraTools) {
78
+ for (const tool of input.extraTools) {
79
+ toolMap[tool.name] = true;
80
+ }
81
+ }
82
+ const skills = [];
83
+ for (const ref of input.danceRefs) {
84
+ skills.push(await compileDance(input.executionDir, ref, workspaceHash, input.performerId, input.executionDir, input.scope || 'workspace', input.actId));
85
+ }
86
+ const requestTargets = (input.requestTargets || []).map((target) => ({
87
+ performerId: target.performerId,
88
+ performerName: target.performerName,
89
+ agentName: getProjectedAgentName(input.workingDir, target.performerId, 'build', input.scope, input.actId),
90
+ description: target.description || '',
91
+ }));
92
+ const requestProjection = compileMentionRelations(requestTargets);
93
+ const compileScope = input.scope === 'workspace' ? 'stage' : input.scope;
94
+ const compiled = await compilePerformer(input.executionDir, {
95
+ performerId: input.performerId,
96
+ performerName: input.performerName,
97
+ talRef: input.talRef,
98
+ model: input.model,
99
+ modelVariant: input.modelVariant || null,
100
+ workspaceHash,
101
+ executionDir: input.executionDir,
102
+ scope: compileScope || 'stage',
103
+ actId: input.actId,
104
+ skillNames: skills.map((skill) => skill.logicalName),
105
+ toolMap,
106
+ taskAllowlist: requestProjection.taskAllowlist,
107
+ collaborationPromptSection: input.collaborationPromptSection || null,
108
+ relationPromptSection: requestProjection.promptSection,
109
+ }, skills);
110
+ let changed = false;
111
+ if (input.extraTools) {
112
+ // Clean stale act tool files that don't belong to the current extra tools set.
113
+ // This prevents zombie tools from deleted/renamed acts lingering in OpenCode's cache.
114
+ const currentToolNames = new Set(input.extraTools.map((t) => t.name));
115
+ const collaborationToolNames = new Set([
116
+ ...COLLABORATION_TOOL_NAMES,
117
+ ...LEGACY_COLLABORATION_TOOL_NAMES,
118
+ ]);
119
+ const toolsDir = path.join(input.executionDir, '.opencode', 'tools');
120
+ try {
121
+ const existing = await fs.readdir(toolsDir);
122
+ for (const file of existing) {
123
+ if (file.endsWith('.ts')) {
124
+ const toolName = file.replace(/\.ts$/, '');
125
+ if (collaborationToolNames.has(toolName) && !currentToolNames.has(toolName)) {
126
+ await fs.rm(path.join(toolsDir, file), { force: true }).catch(() => { });
127
+ changed = true;
128
+ }
129
+ }
130
+ }
131
+ }
132
+ catch {
133
+ // tools dir may not exist yet
134
+ }
135
+ for (const tool of input.extraTools) {
136
+ const toolPath = path.join(input.executionDir, '.opencode', 'tools', `${tool.name}.ts`);
137
+ compiled.allFiles.push(toolPath);
138
+ changed = (await writeIfChanged(toolPath, tool.content)) || changed;
139
+ }
140
+ }
141
+ await cleanGroupFiles(input.executionDir, groupKey(input.performerId), compiled.allFiles);
142
+ for (const skill of skills) {
143
+ changed = (await writeIfChanged(skill.filePath, skill.content)) || changed;
144
+ // Track additional bundle files (scripts/, references/, assets/) in the manifest.
145
+ // copyBundleSiblings already wrote these files; we just need to check if any
146
+ // were new or updated so that opencode reloads when bundle content changes.
147
+ if (skill.additionalFiles.length > 0) {
148
+ compiled.allFiles.push(...skill.additionalFiles);
149
+ // Mark changed if any extra file is newer than the agent file (proxy for freshness)
150
+ if (!changed) {
151
+ for (const extra of skill.additionalFiles) {
152
+ const stat = await fs.stat(extra).catch(() => null);
153
+ const agentStat = await fs.stat(skill.filePath).catch(() => null);
154
+ if (stat && agentStat && stat.mtimeMs > agentStat.mtimeMs) {
155
+ changed = true;
156
+ break;
157
+ }
158
+ }
159
+ }
160
+ }
161
+ }
162
+ changed = (await writeIfChanged(compiled.agentPaths.build, compiled.agentContents.build)) || changed;
163
+ if (compiled.agentPaths.plan && compiled.agentContents.plan) {
164
+ changed = (await writeIfChanged(compiled.agentPaths.plan, compiled.agentContents.plan)) || changed;
165
+ }
166
+ await updateManifestGroup(input.executionDir, workspaceHash, groupKey(input.performerId), compiled.allFiles);
167
+ await updateGitExclude(input.executionDir);
168
+ if (changed) {
169
+ const oc = await getOpencode();
170
+ await oc.instance.dispose({ directory: input.executionDir }).catch(() => { });
171
+ }
172
+ return {
173
+ compiled,
174
+ toolResolution,
175
+ capabilitySnapshot: await resolveCapabilitySnapshot(input.executionDir, input.model),
176
+ };
177
+ }
178
+ export function getProjectedAgentName(workingDir, performerId, posture, scope = 'workspace', actId) {
179
+ const workspaceHash = computeWorkspaceHash(workingDir);
180
+ return resolveAgentIdentity({
181
+ executionDir: workingDir,
182
+ workspaceHash,
183
+ performerId,
184
+ posture,
185
+ scope,
186
+ actId,
187
+ }).agentName;
188
+ }