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,313 @@
1
+ import { nanoid } from 'nanoid';
2
+ import { SafetyGuard } from './safety-guard.js';
3
+ import { ThreadManager } from './thread-manager.js';
4
+ import { clearParticipantCircuit, clearParticipantQueueRunning, drainParticipantQueueAfterSettlement, markParticipantQueueRunning, processWakeCascade, tripParticipantCircuit, } from './wake-cascade.js';
5
+ import { workspaceIdForDir } from '../../lib/config.js';
6
+ function errorMessage(error) {
7
+ return error instanceof Error ? error.message : 'Unknown error';
8
+ }
9
+ class ActRuntimeService {
10
+ threadManager;
11
+ workingDir;
12
+ safetyGuards = new Map();
13
+ _threadsLoaded = false;
14
+ constructor(workspaceId, workingDir) {
15
+ this.workingDir = workingDir;
16
+ this.threadManager = new ThreadManager(workspaceId, workingDir);
17
+ }
18
+ /** Lazy-load persisted threads on first access */
19
+ async ensureThreadsLoaded() {
20
+ if (this._threadsLoaded)
21
+ return;
22
+ this._threadsLoaded = true;
23
+ console.log(`[act-runtime] Loading persisted threads for workspace ${this.workingDir}`);
24
+ await this.threadManager.loadPersistedThreads();
25
+ console.log(`[act-runtime] Loaded ${this.threadManager.getActiveThreadCount()} threads`);
26
+ }
27
+ getSafetyGuard(threadId) {
28
+ if (!this.safetyGuards.has(threadId)) {
29
+ const actDef = this.threadManager.getActDefinition(threadId);
30
+ this.safetyGuards.set(threadId, SafetyGuard.fromActSafety(actDef?.safety));
31
+ }
32
+ return this.safetyGuards.get(threadId);
33
+ }
34
+ async sendMessage(threadId, body) {
35
+ await this.ensureThreadsLoaded();
36
+ const runtime = this.threadManager.getThreadRuntime(threadId);
37
+ if (!runtime) {
38
+ console.error(`[act-runtime] sendMessage: Thread ${threadId} not found. workingDir=${this.workingDir}`);
39
+ return { ok: false, status: 404, error: `Thread ${threadId} not found` };
40
+ }
41
+ const guard = this.getSafetyGuard(threadId);
42
+ // Thread timeout check (PRD §16.3)
43
+ const timeoutCheck = guard.checkTimeout(runtime.thread);
44
+ if (!timeoutCheck.ok) {
45
+ return { ok: false, status: 429, error: timeoutCheck.reason };
46
+ }
47
+ // Event budget check BEFORE message is added (PRD §16.1)
48
+ const preEvent = {
49
+ id: nanoid(),
50
+ type: 'message.sent',
51
+ sourceType: 'performer',
52
+ source: body.from,
53
+ timestamp: Date.now(),
54
+ payload: { from: body.from, to: body.to, tag: body.tag, threadId },
55
+ };
56
+ const budgetCheck = guard.checkEventBudget(preEvent);
57
+ if (!budgetCheck.ok) {
58
+ return { ok: false, status: 429, error: budgetCheck.reason };
59
+ }
60
+ // Relation permission check (PRD §16.5)
61
+ const actDefinition = this.threadManager.getActDefinition(threadId);
62
+ if (actDefinition) {
63
+ const permCheck = guard.checkPermission(body.from, body.to, actDefinition.relations);
64
+ if (!permCheck.ok) {
65
+ return { ok: false, status: 403, error: permCheck.reason };
66
+ }
67
+ }
68
+ const pairCheck = guard.checkPairBudget(body.from, body.to);
69
+ if (!pairCheck.ok) {
70
+ return { ok: false, status: 429, error: pairCheck.reason };
71
+ }
72
+ const loopCheck = guard.checkLoopDetection(body.from, body.to);
73
+ if (!loopCheck.ok) {
74
+ return { ok: false, status: 429, error: loopCheck.reason };
75
+ }
76
+ const message = runtime.mailbox.addMessage({
77
+ from: body.from,
78
+ to: body.to,
79
+ content: body.content,
80
+ tag: body.tag,
81
+ threadId,
82
+ });
83
+ // Update event with messageId and log
84
+ preEvent.payload = { messageId: message.id, from: body.from, to: body.to, tag: body.tag, threadId };
85
+ await this.threadManager.logEvent(threadId, preEvent);
86
+ // Fire-and-forget: don't block the tool call response
87
+ if (actDefinition) {
88
+ processWakeCascade(preEvent, actDefinition, runtime.mailbox, this.threadManager, threadId, this.workingDir)
89
+ .then((cascadeResult) => this.maybeEmitRuntimeIdle(threadId, cascadeResult, actDefinition, runtime.mailbox))
90
+ .catch((err) => console.error('[act-runtime] Wake cascade error (sendMessage):', err));
91
+ }
92
+ return { ok: true, messageId: message.id };
93
+ }
94
+ async postToBoard(threadId, body) {
95
+ await this.ensureThreadsLoaded();
96
+ const runtime = this.threadManager.getThreadRuntime(threadId);
97
+ if (!runtime) {
98
+ return { ok: false, status: 404, error: `Thread ${threadId} not found` };
99
+ }
100
+ const guard = this.getSafetyGuard(threadId);
101
+ // Thread timeout check (PRD §16.3)
102
+ const timeoutCheck = guard.checkTimeout(runtime.thread);
103
+ if (!timeoutCheck.ok) {
104
+ return { ok: false, status: 429, error: timeoutCheck.reason };
105
+ }
106
+ const boardCheck = guard.checkBoardUpdateBudget(body.key);
107
+ if (!boardCheck.ok) {
108
+ return { ok: false, status: 429, error: boardCheck.reason };
109
+ }
110
+ // Board writePolicy check (PRD §16.6)
111
+ const existingEntry = runtime.mailbox.readBoard(body.key);
112
+ if (existingEntry) {
113
+ const wpCheck = guard.checkBoardWritePolicy(existingEntry, body.author);
114
+ if (!wpCheck.ok) {
115
+ return { ok: false, status: 403, error: wpCheck.reason };
116
+ }
117
+ }
118
+ try {
119
+ const entry = runtime.mailbox.postToBoard({
120
+ key: body.key,
121
+ kind: body.kind,
122
+ author: body.author,
123
+ content: body.content,
124
+ updateMode: body.updateMode || 'replace',
125
+ ownership: 'authoritative',
126
+ metadata: body.metadata,
127
+ threadId,
128
+ });
129
+ await this.threadManager.persistBoard(threadId);
130
+ const existing = runtime.mailbox.readBoard(body.key);
131
+ const eventType = (existing?.version ?? 0) > 1 ? 'board.updated' : 'board.posted';
132
+ const event = {
133
+ id: nanoid(),
134
+ type: eventType,
135
+ sourceType: 'performer',
136
+ source: body.author,
137
+ timestamp: Date.now(),
138
+ payload: { key: body.key, kind: body.kind, author: body.author, threadId },
139
+ };
140
+ await this.threadManager.logEvent(threadId, event);
141
+ const actDefinition = this.threadManager.getActDefinition(threadId);
142
+ // Fire-and-forget: don't block the tool call response
143
+ if (actDefinition) {
144
+ processWakeCascade(event, actDefinition, runtime.mailbox, this.threadManager, threadId, this.workingDir)
145
+ .then((cascadeResult) => this.maybeEmitRuntimeIdle(threadId, cascadeResult, actDefinition, runtime.mailbox))
146
+ .catch((err) => console.error('[act-runtime] Wake cascade error (postToBoard):', err));
147
+ }
148
+ return { ok: true, entryId: entry.id, version: entry.version };
149
+ }
150
+ catch (error) {
151
+ return { ok: false, status: 403, error: errorMessage(error) };
152
+ }
153
+ }
154
+ async readBoard(threadId, key) {
155
+ await this.ensureThreadsLoaded();
156
+ const runtime = this.threadManager.getThreadRuntime(threadId);
157
+ if (!runtime) {
158
+ return { ok: false, status: 404, error: `Thread ${threadId} not found` };
159
+ }
160
+ if (key) {
161
+ const entry = runtime.mailbox.readBoard(key);
162
+ return { ok: true, entries: entry ? [entry] : [] };
163
+ }
164
+ return { ok: true, entries: runtime.mailbox.getBoardSnapshot() };
165
+ }
166
+ async syncActDefinition(actId, actDefinition) {
167
+ await this.ensureThreadsLoaded();
168
+ const threadIds = this.threadManager.listThreadIds(actId, ['active', 'idle']);
169
+ for (const threadId of threadIds) {
170
+ const synced = await this.threadManager.syncThreadActDefinition(threadId, actDefinition);
171
+ if (!synced)
172
+ continue;
173
+ this.safetyGuards.delete(threadId);
174
+ const event = {
175
+ id: nanoid(),
176
+ type: 'runtime.reconfigured',
177
+ sourceType: 'system',
178
+ source: 'studio',
179
+ timestamp: Date.now(),
180
+ payload: {
181
+ actId,
182
+ threadId,
183
+ participantCount: Object.keys(actDefinition.participants || {}).length,
184
+ relationCount: actDefinition.relations.length,
185
+ },
186
+ };
187
+ await this.threadManager.logEvent(threadId, event);
188
+ }
189
+ return { ok: true, threads: this.threadManager.listThreads(actId) };
190
+ }
191
+ async setWakeCondition(threadId, body) {
192
+ await this.ensureThreadsLoaded();
193
+ const runtime = this.threadManager.getThreadRuntime(threadId);
194
+ if (!runtime) {
195
+ return { ok: false, status: 404, error: `Thread ${threadId} not found` };
196
+ }
197
+ const wakeCondition = runtime.mailbox.addWakeCondition({
198
+ target: body.target,
199
+ createdBy: body.createdBy,
200
+ onSatisfiedMessage: body.onSatisfiedMessage,
201
+ condition: body.condition,
202
+ });
203
+ return { ok: true, conditionId: wakeCondition.id };
204
+ }
205
+ async createThread(actId, actDefinition) {
206
+ const thread = await this.threadManager.createThread(actId, actDefinition);
207
+ return { ok: true, thread };
208
+ }
209
+ async getActDefinition(threadId) {
210
+ await this.ensureThreadsLoaded();
211
+ return this.threadManager.getActDefinition(threadId);
212
+ }
213
+ async listThreads(actId) {
214
+ await this.ensureThreadsLoaded();
215
+ return { ok: true, threads: this.threadManager.listThreads(actId) };
216
+ }
217
+ async deleteThread(_actId, threadId) {
218
+ const deleted = await this.threadManager.deleteThread(threadId);
219
+ if (!deleted) {
220
+ return { ok: false, status: 404, error: `Thread ${threadId} not found` };
221
+ }
222
+ return { ok: true };
223
+ }
224
+ async getThread(threadId) {
225
+ await this.ensureThreadsLoaded();
226
+ const thread = this.threadManager.getThread(threadId);
227
+ if (!thread) {
228
+ return { ok: false, status: 404, error: `Thread ${threadId} not found` };
229
+ }
230
+ return { ok: true, thread };
231
+ }
232
+ async getRecentEvents(threadId, count = 50) {
233
+ await this.ensureThreadsLoaded();
234
+ const events = await this.threadManager.getRecentEvents(threadId, count);
235
+ return { ok: true, events };
236
+ }
237
+ async registerParticipantSession(threadId, participantKey, sessionId) {
238
+ await this.ensureThreadsLoaded();
239
+ await this.threadManager.getOrCreateSession(threadId, participantKey, () => sessionId);
240
+ }
241
+ async beginUserTurn(threadId) {
242
+ await this.ensureThreadsLoaded();
243
+ this.getSafetyGuard(threadId).reset(Date.now());
244
+ }
245
+ async markParticipantSessionBusy(threadId, participantKey) {
246
+ await this.ensureThreadsLoaded();
247
+ markParticipantQueueRunning(threadId, participantKey);
248
+ }
249
+ async clearParticipantSessionBusy(threadId, participantKey) {
250
+ await this.ensureThreadsLoaded();
251
+ clearParticipantQueueRunning(threadId, participantKey);
252
+ }
253
+ async tripParticipantAutoWakeCircuit(threadId, participantKey, reason) {
254
+ await this.ensureThreadsLoaded();
255
+ tripParticipantCircuit(threadId, participantKey, reason);
256
+ }
257
+ async clearParticipantAutoWakeCircuit(threadId, participantKey) {
258
+ await this.ensureThreadsLoaded();
259
+ clearParticipantCircuit(threadId, participantKey);
260
+ }
261
+ async drainParticipantQueue(threadId, participantKey) {
262
+ await this.ensureThreadsLoaded();
263
+ const runtime = this.threadManager.getThreadRuntime(threadId);
264
+ const actDefinition = this.threadManager.getActDefinition(threadId);
265
+ if (!runtime || !actDefinition) {
266
+ return;
267
+ }
268
+ await drainParticipantQueueAfterSettlement(participantKey, actDefinition, runtime.mailbox, this.threadManager, threadId, this.workingDir);
269
+ }
270
+ /**
271
+ * Emit runtime.idle event when a wake cascade completes with no errors
272
+ * and no queued targets remaining. Participants subscribed to 'runtime.idle'
273
+ * will be woken by this event.
274
+ */
275
+ async maybeEmitRuntimeIdle(threadId, cascadeResult, actDefinition, mailbox) {
276
+ // Only emit if cascade had targets but no errors, and queue is empty
277
+ if (cascadeResult.errors.length > 0)
278
+ return;
279
+ if (cascadeResult.injected.length === 0 && cascadeResult.queued.length === 0)
280
+ return;
281
+ const idleEvent = {
282
+ id: nanoid(),
283
+ type: 'runtime.idle',
284
+ sourceType: 'system',
285
+ source: 'runtime',
286
+ timestamp: Date.now(),
287
+ payload: {
288
+ threadId,
289
+ injectedCount: cascadeResult.injected.length,
290
+ },
291
+ };
292
+ await this.threadManager.logEvent(threadId, idleEvent);
293
+ // Route the idle event — but don't recurse further to avoid infinite loops
294
+ const runtime = this.threadManager.getThreadRuntime(threadId);
295
+ if (runtime) {
296
+ processWakeCascade(idleEvent, actDefinition, mailbox, this.threadManager, threadId, this.workingDir)
297
+ .catch((err) => console.error('[act-runtime] Wake cascade error (runtime.idle):', err));
298
+ }
299
+ }
300
+ }
301
+ const runtimeServices = new Map();
302
+ export function getActRuntimeService(workingDir) {
303
+ const workspaceId = workspaceIdForDir(workingDir);
304
+ let service = runtimeServices.get(workspaceId);
305
+ if (!service) {
306
+ service = new ActRuntimeService(workspaceId, workingDir);
307
+ runtimeServices.set(workspaceId, service);
308
+ }
309
+ return service;
310
+ }
311
+ export async function getActDefinitionForThread(workingDir, threadId) {
312
+ return getActRuntimeService(workingDir).getActDefinition(threadId);
313
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * act-runtime-utils.ts — Shared utilities for Act runtime modules.
3
+ */
4
+ /**
5
+ * Safely extract a string value from an untyped payload record.
6
+ */
7
+ export function payloadString(payload, key) {
8
+ const value = payload[key];
9
+ return typeof value === 'string' ? value : undefined;
10
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * act-tool-projection.ts — Act tool projection for participant sessions
3
+ *
4
+ * Projects collaboration tools and stable collaboration context into participant sessions.
5
+ *
6
+ * Tools are generic static files. Stable collaboration context is intended
7
+ * for agent/system-level injection rather than per-turn user prompt injection.
8
+ */
9
+ import { getStaticActTools } from './act-tools.js';
10
+ import { buildActContext } from './act-context-builder.js';
11
+ // ── Projection ──────────────────────────────────────────
12
+ /**
13
+ * Generate Act tool projection for a participant in a thread.
14
+ * Creates tool definitions and stable collaboration context.
15
+ */
16
+ export function projectActTools(participantKey, actDefinition, threadId, workingDir) {
17
+ void threadId;
18
+ // 1. Stable collaboration context for agent/system injection
19
+ const contextPrompt = buildActContext(actDefinition, participantKey);
20
+ // 2. Static session-bound tool definitions (only workingDir is baked in)
21
+ const tools = getStaticActTools(workingDir);
22
+ return {
23
+ contextPrompt,
24
+ tools,
25
+ };
26
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * act-tools.ts — Act runtime custom tool definitions
3
+ *
4
+ * PRD §13: Collaboration tools exposed to participants.
5
+ * - message_teammate
6
+ * - update_shared_board
7
+ * - read_shared_board
8
+ * - wait_until
9
+ *
10
+ * These are 4 generic static tool files placed in .opencode/tools/.
11
+ * The model only provides high-level collaboration inputs.
12
+ * Act/thread/participant identity is resolved from the current session.
13
+ */
14
+ import { PORT } from '../../lib/config.js';
15
+ export const COLLABORATION_TOOL_NAMES = [
16
+ 'message_teammate',
17
+ 'update_shared_board',
18
+ 'read_shared_board',
19
+ 'wait_until',
20
+ ];
21
+ export const LEGACY_COLLABORATION_TOOL_NAMES = [
22
+ 'act_send_message',
23
+ 'act_post_to_board',
24
+ 'act_read_board',
25
+ 'act_set_wake_condition',
26
+ ];
27
+ // ── Static tool definitions ─────────────────────────────
28
+ /**
29
+ * The 4 generic Act runtime tools.
30
+ * These are static and session-bound.
31
+ * The workingDir is baked in at write time (changes per workspace, not per thread).
32
+ */
33
+ export function getStaticActTools(workingDir) {
34
+ const wd = encodeURIComponent(workingDir);
35
+ const base = `http://localhost:${PORT}`;
36
+ return [
37
+ {
38
+ name: 'message_teammate',
39
+ content: `import { tool } from "@opencode-ai/plugin"
40
+
41
+ export default tool({
42
+ description: "Send a direct message to a teammate. Use this for targeted coordination when only one teammate needs the update.",
43
+ args: {
44
+ recipient: tool.schema.string().describe("Teammate name to message directly"),
45
+ message: tool.schema.string().describe("Message to send"),
46
+ tag: tool.schema.string().optional().describe("Optional short label for the message"),
47
+ },
48
+ async execute(args, context) {
49
+ const sessionID = context.sessionID
50
+ const res = await fetch(\`${base}/api/act/session/\${encodeURIComponent(sessionID)}/message-teammate?workingDir=${wd}\`, {
51
+ method: "POST",
52
+ headers: { "Content-Type": "application/json" },
53
+ body: JSON.stringify({
54
+ recipient: args.recipient,
55
+ message: args.message,
56
+ tag: args.tag,
57
+ }),
58
+ })
59
+ const data = await res.json()
60
+ if (!data.ok) return data.error
61
+ return "Direct message sent."
62
+ },
63
+ })
64
+ `,
65
+ },
66
+ {
67
+ name: 'update_shared_board',
68
+ content: `import { tool } from "@opencode-ai/plugin"
69
+
70
+ export default tool({
71
+ description: "Create or update a shared note for the whole team. Use this for durable context, decisions, findings, and handoffs.",
72
+ args: {
73
+ entryKey: tool.schema.string().describe("Stable key for the shared note, such as api-spec or review-report"),
74
+ entryType: tool.schema.enum(["artifact", "fact", "task"]).describe("Type of shared note"),
75
+ content: tool.schema.string().describe("Entry content"),
76
+ mode: tool.schema.enum(["replace", "append"]).optional().describe("Whether to replace or append to an existing shared note"),
77
+ },
78
+ async execute(args, context) {
79
+ const sessionID = context.sessionID
80
+ const res = await fetch(\`${base}/api/act/session/\${encodeURIComponent(sessionID)}/update-shared-board?workingDir=${wd}\`, {
81
+ method: "POST",
82
+ headers: { "Content-Type": "application/json" },
83
+ body: JSON.stringify({
84
+ entryKey: args.entryKey,
85
+ entryType: args.entryType,
86
+ content: args.content,
87
+ mode: args.mode,
88
+ }),
89
+ })
90
+ const data = await res.json()
91
+ if (!data.ok) return data.error
92
+ return "Shared note updated."
93
+ },
94
+ })
95
+ `,
96
+ },
97
+ {
98
+ name: 'read_shared_board',
99
+ content: `import { tool } from "@opencode-ai/plugin"
100
+
101
+ export default tool({
102
+ description: "Read shared notes created by the team. Returns matching entries as JSON.",
103
+ args: {
104
+ entryKey: tool.schema.string().optional().describe("Optional shared note key. Omit to read all shared notes."),
105
+ },
106
+ async execute(args, context) {
107
+ const sessionID = context.sessionID
108
+ const params = args.entryKey ? "&key=" + encodeURIComponent(args.entryKey) : ""
109
+ const res = await fetch(\`${base}/api/act/session/\${encodeURIComponent(sessionID)}/read-shared-board?workingDir=${wd}\` + params)
110
+ const data = await res.json()
111
+ if (!data.ok) return data.error
112
+ return JSON.stringify(data.entries, null, 2)
113
+ },
114
+ })
115
+ `,
116
+ },
117
+ {
118
+ name: 'wait_until',
119
+ content: `import { tool } from "@opencode-ai/plugin"
120
+
121
+ export default tool({
122
+ description: "Pause until a condition is met, then resume with the provided instruction. Use this when you are waiting for more input or a shared update.",
123
+ args: {
124
+ resumeWith: tool.schema.string().describe("Instruction to receive when the wait is over"),
125
+ conditionJson: tool.schema.string().describe("JSON condition expression. Supported types: all_of, any_of, board_key_exists, message_received, timeout"),
126
+ },
127
+ async execute(args, context) {
128
+ let condition
129
+ try {
130
+ condition = JSON.parse(args.conditionJson)
131
+ } catch {
132
+ return "Error: conditionJson must be valid JSON"
133
+ }
134
+ const sessionID = context.sessionID
135
+ const res = await fetch(\`${base}/api/act/session/\${encodeURIComponent(sessionID)}/wait-until?workingDir=${wd}\`, {
136
+ method: "POST",
137
+ headers: { "Content-Type": "application/json" },
138
+ body: JSON.stringify({
139
+ resumeWith: args.resumeWith,
140
+ condition,
141
+ }),
142
+ })
143
+ const data = await res.json()
144
+ if (!data.ok) return data.error
145
+ return "Wait condition saved. You will resume when it is satisfied."
146
+ },
147
+ })
148
+ `,
149
+ },
150
+ ];
151
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * board-persistence.ts — Board file persistence
3
+ *
4
+ * PRD §6.2: Board is durable — persisted to file and survives shutdown.
5
+ * Path: ~/.dot-studio/workspaces/<workspaceId>/act-runtime/<actId>/<threadId>/board.json
6
+ */
7
+ import { promises as fs } from 'node:fs';
8
+ import { join, dirname } from 'node:path';
9
+ import { workspaceActRuntimeDir } from '../../lib/config.js';
10
+ function isErrnoException(error) {
11
+ return error instanceof Error;
12
+ }
13
+ function boardFilePath(workspaceId, actId, threadId) {
14
+ return join(workspaceActRuntimeDir(workspaceId, actId, threadId), 'board.json');
15
+ }
16
+ /**
17
+ * Save board entries to file.
18
+ */
19
+ export async function saveBoardToFile(workspaceId, actId, threadId, entries) {
20
+ const filePath = boardFilePath(workspaceId, actId, threadId);
21
+ await fs.mkdir(dirname(filePath), { recursive: true });
22
+ await fs.writeFile(filePath, JSON.stringify(entries, null, 2), 'utf-8');
23
+ }
24
+ /**
25
+ * Load board entries from file.
26
+ */
27
+ export async function loadBoardFromFile(workspaceId, actId, threadId) {
28
+ const filePath = boardFilePath(workspaceId, actId, threadId);
29
+ try {
30
+ const content = await fs.readFile(filePath, 'utf-8');
31
+ return JSON.parse(content);
32
+ }
33
+ catch (error) {
34
+ if (isErrnoException(error) && error.code === 'ENOENT')
35
+ return [];
36
+ throw error;
37
+ }
38
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * event-logger.ts — Append-only event log for Act Thread
3
+ *
4
+ * PRD §6.3: Events are stored as append-only .jsonl files.
5
+ * Path: ~/.dot-studio/workspaces/<workspaceId>/act-runtime/<actId>/<threadId>/events.jsonl
6
+ */
7
+ import { promises as fs } from 'node:fs';
8
+ import { join, dirname } from 'node:path';
9
+ import { workspaceActRuntimeDir } from '../../lib/config.js';
10
+ function isErrnoException(error) {
11
+ return error instanceof Error;
12
+ }
13
+ // ── EventLogger ─────────────────────────────────────────
14
+ export class EventLogger {
15
+ logPath;
16
+ _workspaceId;
17
+ _actId;
18
+ _threadId;
19
+ constructor(workspaceId, actId, threadId) {
20
+ this._workspaceId = workspaceId;
21
+ this._actId = actId;
22
+ this._threadId = threadId;
23
+ this.logPath = join(workspaceActRuntimeDir(workspaceId, actId, threadId), 'events.jsonl');
24
+ }
25
+ /**
26
+ * Append an event to the log file.
27
+ */
28
+ async appendEvent(event) {
29
+ await fs.mkdir(dirname(this.logPath), { recursive: true });
30
+ const line = JSON.stringify(event) + '\n';
31
+ await fs.appendFile(this.logPath, line, 'utf-8');
32
+ }
33
+ /**
34
+ * Read the last N events from the log (for UI Activity View).
35
+ */
36
+ async tailEvents(count) {
37
+ try {
38
+ const content = await fs.readFile(this.logPath, 'utf-8');
39
+ const lines = content.trim().split('\n').filter(Boolean);
40
+ const tail = lines.slice(-count);
41
+ return tail.map((line) => JSON.parse(line));
42
+ }
43
+ catch (error) {
44
+ if (isErrnoException(error) && error.code === 'ENOENT')
45
+ return [];
46
+ throw error;
47
+ }
48
+ }
49
+ /**
50
+ * Read all events from the log.
51
+ */
52
+ async readAllEvents() {
53
+ try {
54
+ const content = await fs.readFile(this.logPath, 'utf-8');
55
+ const lines = content.trim().split('\n').filter(Boolean);
56
+ return lines.map((line) => JSON.parse(line));
57
+ }
58
+ catch (error) {
59
+ if (isErrnoException(error) && error.code === 'ENOENT')
60
+ return [];
61
+ throw error;
62
+ }
63
+ }
64
+ /**
65
+ * Get the file path for this event log.
66
+ */
67
+ getLogPath() {
68
+ return this.logPath;
69
+ }
70
+ get workspaceId() { return this._workspaceId; }
71
+ get actId() { return this._actId; }
72
+ get threadId() { return this._threadId; }
73
+ }