@flowdular/sdk 0.1.0 → 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 (174) hide show
  1. package/.ai/skills/test-hardening/SKILL.md +1 -1
  2. package/README.md +69 -12
  3. package/assets/flowdular-banner.png +0 -0
  4. package/modules/agents/package.json +1 -1
  5. package/modules/auth/package.json +1 -1
  6. package/modules/auth/src/client/AuthenticationCore.tsrx +3 -1
  7. package/modules/auth/src/server/endpoints.ts +3 -1
  8. package/modules/auth/src/server/runtime.ts +4 -0
  9. package/modules/automations/package.json +1 -1
  10. package/modules/automations-workflows-integration/package.json +1 -1
  11. package/modules/profile/package.json +1 -1
  12. package/modules/sandbox/package.json +1 -1
  13. package/modules/system/package.json +1 -1
  14. package/modules/users/package.json +1 -1
  15. package/modules/workflows/package.json +1 -1
  16. package/package.json +5 -13
  17. package/packages/ai-provider/package.json +1 -1
  18. package/packages/cli-protocol/package.json +1 -1
  19. package/packages/client/package.json +1 -1
  20. package/packages/client/src/routing.ts +39 -2
  21. package/packages/client/src/shell/BrandMark.tsrx +5 -1
  22. package/packages/client/src/shell/navigation.ts +3 -1
  23. package/packages/client/src/state.ts +6 -2
  24. package/packages/contracts/package.json +1 -1
  25. package/packages/contracts/schemas/project.schema.json +16 -42
  26. package/packages/database/package.json +1 -1
  27. package/packages/database-pglite/package.json +1 -1
  28. package/packages/database-testing/package.json +1 -1
  29. package/packages/dev-console/package.json +1 -1
  30. package/packages/harness/package.json +1 -1
  31. package/packages/kernel/package.json +1 -1
  32. package/packages/server/package.json +1 -1
  33. package/packages/server/src/application-routes.ts +80 -8
  34. package/packages/server/src/index.ts +4 -1
  35. package/packages/server/src/web-html.ts +9 -5
  36. package/packages/server/src/web.ts +59 -7
  37. package/packages/ui/package.json +1 -1
  38. package/packages/coding-agent/package.json +0 -44
  39. package/packages/coding-agent/src/drivers/byok.ts +0 -322
  40. package/packages/coding-agent/src/drivers/claude-code.ts +0 -290
  41. package/packages/coding-agent/src/drivers/codex.ts +0 -281
  42. package/packages/coding-agent/src/index.ts +0 -56
  43. package/packages/coding-agent/src/registry.ts +0 -119
  44. package/packages/coding-agent/src/roles/contract.ts +0 -60
  45. package/packages/coding-agent/src/roles/defaults.ts +0 -134
  46. package/packages/coding-agent/src/roles/handoff.ts +0 -41
  47. package/packages/coding-agent/src/roles/registry.ts +0 -156
  48. package/packages/coding-agent/src/roles/skills.ts +0 -72
  49. package/packages/coding-agent/src/roles/sync.ts +0 -88
  50. package/packages/coding-agent/src/types.ts +0 -102
  51. package/packages/coding-agent/src/workspace.ts +0 -275
  52. package/packages/sandbox/README.md +0 -691
  53. package/packages/sandbox/bin/flowdular-sandbox.mjs +0 -393
  54. package/packages/sandbox/dist/client/assets/App-ClMOrpyg.js +0 -11
  55. package/packages/sandbox/dist/client/assets/PreviewHost-BHiHrfbQ.js +0 -1
  56. package/packages/sandbox/dist/client/assets/auto-buGEq6de.js +0 -3
  57. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  58. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  59. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  60. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  61. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  62. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  63. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  64. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  65. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  66. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  67. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  68. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  69. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  70. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  71. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  72. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  73. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  74. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  75. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  76. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  77. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
  78. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
  79. package/packages/sandbox/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
  80. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
  81. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
  82. package/packages/sandbox/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
  83. package/packages/sandbox/dist/client/assets/index-CvwhlQ87.js +0 -4
  84. package/packages/sandbox/dist/client/assets/styles-BPWaGf55.js +0 -3
  85. package/packages/sandbox/dist/client/assets/styles-Dx1_KOag.css +0 -1
  86. package/packages/sandbox/dist/client/favicon.svg +0 -1
  87. package/packages/sandbox/dist/server/assets/auto-CVGNYyb3.js +0 -12785
  88. package/packages/sandbox/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +0 -4125
  89. package/packages/sandbox/dist/server/assets/nodefs-DVSI7kG0.js +0 -24
  90. package/packages/sandbox/dist/server/assets/opfs-ahp-D56iokt1.js +0 -388
  91. package/packages/sandbox/dist/server/assets/token-Bvaq7Anz.js +0 -55
  92. package/packages/sandbox/dist/server/assets/token-util-BqSvdIPQ.js +0 -3
  93. package/packages/sandbox/dist/server/assets/token-util-CktFFT4z.js +0 -494
  94. package/packages/sandbox/dist/server/entry.js +0 -91671
  95. package/packages/sandbox/dist/server/favicon.svg +0 -1
  96. package/packages/sandbox/dist/server/index.html +0 -161
  97. package/packages/sandbox/index.html +0 -159
  98. package/packages/sandbox/octane.config.ts +0 -36
  99. package/packages/sandbox/package.json +0 -83
  100. package/packages/sandbox/public/favicon.svg +0 -1
  101. package/packages/sandbox/public/og.png +0 -0
  102. package/packages/sandbox/src/App.tsrx +0 -924
  103. package/packages/sandbox/src/client/ApprovalHandoff.tsrx +0 -39
  104. package/packages/sandbox/src/client/ChatPane.tsrx +0 -694
  105. package/packages/sandbox/src/client/ConnectView.tsrx +0 -86
  106. package/packages/sandbox/src/client/DiffPane.tsrx +0 -150
  107. package/packages/sandbox/src/client/EjectModal.tsrx +0 -571
  108. package/packages/sandbox/src/client/GateResults.tsrx +0 -60
  109. package/packages/sandbox/src/client/GitHubSettingsModal.tsrx +0 -219
  110. package/packages/sandbox/src/client/NewSessionForm.tsrx +0 -214
  111. package/packages/sandbox/src/client/SessionBar.tsrx +0 -151
  112. package/packages/sandbox/src/client/SessionPicker.tsrx +0 -472
  113. package/packages/sandbox/src/client/SpecEditor.tsrx +0 -109
  114. package/packages/sandbox/src/client/SpecReviewCard.tsrx +0 -321
  115. package/packages/sandbox/src/client/WorkCard.tsrx +0 -128
  116. package/packages/sandbox/src/client/WorkbenchModal.tsrx +0 -161
  117. package/packages/sandbox/src/client/WorkspaceMenu.tsrx +0 -100
  118. package/packages/sandbox/src/client/api.ts +0 -797
  119. package/packages/sandbox/src/client/attachments.ts +0 -73
  120. package/packages/sandbox/src/client/dashboard.ts +0 -46
  121. package/packages/sandbox/src/client/github-settings.ts +0 -48
  122. package/packages/sandbox/src/client/highlight.ts +0 -105
  123. package/packages/sandbox/src/client/i18n.ts +0 -32
  124. package/packages/sandbox/src/client/locales/en.json +0 -466
  125. package/packages/sandbox/src/client/locales/pl.json +0 -466
  126. package/packages/sandbox/src/client/session-labels.ts +0 -77
  127. package/packages/sandbox/src/client/state.ts +0 -108
  128. package/packages/sandbox/src/index.ts +0 -10
  129. package/packages/sandbox/src/preview/PreviewHost.tsrx +0 -373
  130. package/packages/sandbox/src/preview/load-module.ts +0 -16
  131. package/packages/sandbox/src/server/attachments.ts +0 -308
  132. package/packages/sandbox/src/server/auto-review.ts +0 -181
  133. package/packages/sandbox/src/server/checkpoints.ts +0 -90
  134. package/packages/sandbox/src/server/config.ts +0 -460
  135. package/packages/sandbox/src/server/dashboard.ts +0 -245
  136. package/packages/sandbox/src/server/delivery/configuration.ts +0 -193
  137. package/packages/sandbox/src/server/delivery/git-pr.ts +0 -1473
  138. package/packages/sandbox/src/server/delivery/index.ts +0 -83
  139. package/packages/sandbox/src/server/delivery/local.ts +0 -151
  140. package/packages/sandbox/src/server/delivery/official-modules.ts +0 -516
  141. package/packages/sandbox/src/server/delivery/plan.ts +0 -218
  142. package/packages/sandbox/src/server/delivery/policies.ts +0 -239
  143. package/packages/sandbox/src/server/delivery/record.ts +0 -41
  144. package/packages/sandbox/src/server/delivery/steps.ts +0 -464
  145. package/packages/sandbox/src/server/delivery/types.ts +0 -158
  146. package/packages/sandbox/src/server/dependencies.ts +0 -88
  147. package/packages/sandbox/src/server/diff.ts +0 -230
  148. package/packages/sandbox/src/server/gates.ts +0 -388
  149. package/packages/sandbox/src/server/index.ts +0 -139
  150. package/packages/sandbox/src/server/path-guard.ts +0 -282
  151. package/packages/sandbox/src/server/planning.ts +0 -749
  152. package/packages/sandbox/src/server/platform-client.ts +0 -189
  153. package/packages/sandbox/src/server/preview-database-host.ts +0 -340
  154. package/packages/sandbox/src/server/preview-database-protocol.ts +0 -163
  155. package/packages/sandbox/src/server/preview-database-proxy.ts +0 -356
  156. package/packages/sandbox/src/server/preview-database.ts +0 -109
  157. package/packages/sandbox/src/server/preview-ipc.ts +0 -15
  158. package/packages/sandbox/src/server/preview-modules.ts +0 -83
  159. package/packages/sandbox/src/server/preview-revision.ts +0 -44
  160. package/packages/sandbox/src/server/preview-runtime.ts +0 -467
  161. package/packages/sandbox/src/server/preview-worker-manager.ts +0 -436
  162. package/packages/sandbox/src/server/preview-worker.ts +0 -168
  163. package/packages/sandbox/src/server/reference.ts +0 -251
  164. package/packages/sandbox/src/server/routes.ts +0 -2258
  165. package/packages/sandbox/src/server/runtime.ts +0 -188
  166. package/packages/sandbox/src/server/session-owner.ts +0 -23
  167. package/packages/sandbox/src/server/sessions.ts +0 -849
  168. package/packages/sandbox/src/server/spec.ts +0 -471
  169. package/packages/sandbox/src/server/turn-lifecycle.ts +0 -56
  170. package/packages/sandbox/src/server/turns.ts +0 -993
  171. package/packages/sandbox/src/server/workspace-install.ts +0 -266
  172. package/packages/sandbox/src/server/workspace-root.ts +0 -73
  173. package/packages/sandbox/src/styles.css +0 -1573
  174. package/packages/sandbox/vite.config.ts +0 -133
@@ -1,993 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { createHash } from 'node:crypto';
3
- import {
4
- access,
5
- cp,
6
- mkdtemp,
7
- readFile,
8
- readdir,
9
- rm,
10
- stat,
11
- } from 'node:fs/promises';
12
- import { tmpdir } from 'node:os';
13
- import { join, relative } from 'node:path';
14
- import {
15
- composeInstruction,
16
- findRole,
17
- parseHandoff,
18
- selectTaskSkill,
19
- type AgentRoleDefinition,
20
- type CodingAgentEvent,
21
- type CodingAgentMessage,
22
- type CodingAgentRegistry,
23
- type HandoffDeclaration,
24
- } from '@flowdular/sdk/coding-agent';
25
- import {
26
- invalidateAutoReview,
27
- prepareAutoReview,
28
- moduleReviewRevision,
29
- recordAutoReview,
30
- } from './auto-review.ts';
31
- import { attachmentInstruction } from './attachments.ts';
32
- import { captureCheckpoint } from './checkpoints.ts';
33
- import { guardAgentPaths } from './path-guard.ts';
34
- import type { SandboxConfiguration } from './config.ts';
35
- import { diffTrees, type FileDiff } from './diff.ts';
36
- import {
37
- GATE_IDS,
38
- formatDirectory,
39
- isGateId,
40
- runGates,
41
- type GateId,
42
- type GateResult,
43
- } from './gates.ts';
44
- import {
45
- SPEC_OWNER_ROLE,
46
- planHandoff,
47
- planSpecGateHandoff,
48
- routeRole,
49
- type RoutingContext,
50
- } from './planning.ts';
51
- import type { PlatformClient } from './platform-client.ts';
52
- import { listSkills, writeAgentPointer } from './reference.ts';
53
- import {
54
- appendChatEntry,
55
- basePathOf,
56
- findSessionModule,
57
- installSessionDependencies,
58
- modulePathOf,
59
- readChat,
60
- readSession,
61
- sessionPaths,
62
- updateSession,
63
- type ChatEntry,
64
- type HandoffPlan,
65
- type SandboxSession,
66
- type SessionModule,
67
- type SessionPaths,
68
- } from './sessions.ts';
69
- import { isSpecApproved, readSpecGate, type SpecGate } from './spec.ts';
70
- import type { InstallResult } from './workspace-install.ts';
71
- import { SandboxSetupError } from './workspace-root.ts';
72
-
73
- export interface TurnContext {
74
- readonly workspaceRoot: string;
75
- readonly configuration: SandboxConfiguration;
76
- readonly registry: CodingAgentRegistry;
77
- readonly roles: readonly AgentRoleDefinition[];
78
- readonly platform: PlatformClient | null;
79
- /* Installs the session workspace; defaults to pnpm. Tests replace it. */
80
- readonly installDependencies?: (
81
- session: SandboxSession,
82
- ) => Promise<InstallResult>;
83
- /* Runs deterministic sandbox gates. Tests may replace the process boundary
84
- while keeping routing and approval behaviour unchanged. */
85
- readonly executeGates?: (input: {
86
- readonly session: SandboxSession;
87
- readonly gates: readonly GateId[];
88
- readonly modules?: readonly SessionModule[];
89
- }) => Promise<readonly GateResult[]>;
90
- }
91
-
92
- export interface TurnInput {
93
- readonly sessionId: string;
94
- readonly message: string;
95
- readonly role?: string;
96
- /* The draft module directory this turn works in. Defaults to the module the
97
- last handoff named, then to the primary module. */
98
- readonly module?: string;
99
- readonly driver?: string;
100
- readonly signal?: AbortSignal;
101
- }
102
-
103
- /* A diff of one draft module, named by the module directory it belongs to. */
104
- export interface SessionFileDiff extends FileDiff {
105
- readonly module: string;
106
- }
107
-
108
- export interface TurnOutcome {
109
- readonly session: SandboxSession;
110
- readonly diffs: readonly SessionFileDiff[];
111
- readonly gates: readonly GateResult[];
112
- readonly handoff: HandoffPlan;
113
- }
114
-
115
- const MAX_HISTORY_MESSAGES = 20;
116
-
117
- function historyFrom(entries: readonly ChatEntry[]): CodingAgentMessage[] {
118
- /* Orchestrator feedback, gate failures included, is replayed as user input
119
- so the next turn can act on it. */
120
- return entries
121
- .filter(
122
- (entry) =>
123
- entry.kind !== 'event' &&
124
- typeof entry.text === 'string' &&
125
- entry.text.trim().length > 0,
126
- )
127
- .slice(-MAX_HISTORY_MESSAGES)
128
- .map((entry) => ({
129
- role: entry.kind === 'agent' ? ('assistant' as const) : ('user' as const),
130
- text: entry.text!,
131
- }));
132
- }
133
-
134
- async function exists(path: string): Promise<boolean> {
135
- try {
136
- await access(path);
137
- return true;
138
- } catch {
139
- return false;
140
- }
141
- }
142
-
143
- /* Most modules declare their endpoints in src/api/endpoints.ts, but a module
144
- may compose its server from src/platform.ts or several files under
145
- src/server. Any of the three means the server surface exists. */
146
- async function hasServerSurface(modulePath: string): Promise<boolean> {
147
- if (await exists(join(modulePath, 'src/api/endpoints.ts'))) return true;
148
- if (await exists(join(modulePath, 'src/platform.ts'))) return true;
149
- try {
150
- return (await readdir(join(modulePath, 'src/server'))).length > 0;
151
- } catch {
152
- return false;
153
- }
154
- }
155
-
156
- async function routingContext(
157
- context: TurnContext,
158
- session: SandboxSession,
159
- paths: SessionPaths,
160
- modulePath: string,
161
- message: string,
162
- entries: readonly ChatEntry[],
163
- specApproved: boolean | null,
164
- ): Promise<RoutingContext> {
165
- return {
166
- session,
167
- paths,
168
- roles: context.roles,
169
- message,
170
- hasSpec: await exists(join(modulePath, 'spec/module.yaml')),
171
- hasManifest: await exists(join(modulePath, 'module.json')),
172
- hasServer: await hasServerSurface(modulePath),
173
- hasClient: await exists(join(modulePath, 'src/client/contribution.tsrx')),
174
- specApproved,
175
- lastHandoff:
176
- entries.filter((entry) => entry.handoff).at(-1)?.handoff ?? null,
177
- };
178
- }
179
-
180
- /* The last handoff names the module the work continues in, so a chained turn
181
- stays where the previous one left off. An explicit request wins, and one that
182
- names a module the session does not have is refused rather than silently
183
- redirected to the primary. */
184
- function activeModule(
185
- session: SandboxSession,
186
- requested: string | undefined,
187
- entries: readonly ChatEntry[],
188
- ): SessionModule {
189
- if (requested) return findSessionModule(session, requested);
190
- const named = entries.filter((entry) => entry.handoff).at(-1)
191
- ?.handoff?.module;
192
- return (
193
- session.modules.find((module) => module.directory === named) ??
194
- session.modules[0]!
195
- );
196
- }
197
-
198
- /* Nobody but the specification owner works in a module whose specification the
199
- operator has not approved. A missing owner role is a configuration error, not
200
- permission to bypass the gate. */
201
- function refusesUnapproved(gate: SpecGate, roleId: string): boolean {
202
- return gate.approved !== true && roleId !== SPEC_OWNER_ROLE;
203
- }
204
-
205
- /* The refused turn: the operator hears why nothing ran, with a stable code, and
206
- the session stops on the move that unblocks it. */
207
- async function* refuseUnapproved(
208
- context: TurnContext,
209
- session: SandboxSession,
210
- active: SessionModule,
211
- role: AgentRoleDefinition,
212
- gate: SpecGate,
213
- input: { readonly brief: string; readonly driver: string },
214
- ): AsyncGenerator<ChatEntry, TurnOutcome> {
215
- const handoff = planSpecGateHandoff({
216
- roles: context.roles,
217
- module: active.directory,
218
- refusedRole: role.id,
219
- changed: gate.changed,
220
- brief: input.brief,
221
- });
222
- const reason = `${role.name} did not take this turn: the specification of ${active.id} is not approved. ${
223
- gate.changed
224
- ? 'Review the change below and approve it, ask for changes, or edit the specification yourself.'
225
- : `${handoff.roleName} writes the specification change first.`
226
- }`;
227
- yield await appendChatEntry(context.workspaceRoot, session, {
228
- kind: 'system',
229
- role: role.id,
230
- module: active.directory,
231
- text: reason,
232
- event: {
233
- type: 'error',
234
- code: 'SPEC_NOT_APPROVED',
235
- message: reason,
236
- } as CodingAgentEvent,
237
- });
238
- yield await appendChatEntry(context.workspaceRoot, session, {
239
- kind: 'system',
240
- role: handoff.role,
241
- module: active.directory,
242
- text: handoffText(handoff),
243
- handoff,
244
- });
245
- const updated = await updateSession(context.workspaceRoot, session.id, {
246
- state: handoff.kind === 'approval' ? 'awaiting-approval' : 'planned',
247
- driver: input.driver,
248
- });
249
- return {
250
- session: updated,
251
- diffs: await collectDiffs(context.workspaceRoot, updated),
252
- gates: [],
253
- handoff,
254
- };
255
- }
256
-
257
- function handoffText(handoff: HandoffPlan): string {
258
- if (handoff.kind === 'continue') {
259
- return `Handing off to ${handoff.roleName}. ${handoff.reason}`;
260
- }
261
- if (handoff.kind === 'approval') {
262
- return `${handoff.reason} Approve it to let ${handoff.roleName} implement it.`;
263
- }
264
- if (handoff.kind === 'question') {
265
- return `${handoff.reason} Answer below to continue.`;
266
- }
267
- return handoff.reason;
268
- }
269
-
270
- function runCli(
271
- workspaceRoot: string,
272
- args: readonly string[],
273
- ): Promise<{ code: number | null; output: string }> {
274
- return new Promise((resolvePromise) => {
275
- const child = spawn(
276
- 'pnpm',
277
- ['--dir', workspaceRoot, '--silent', 'flowdular', ...args],
278
- {
279
- cwd: workspaceRoot,
280
- env: { ...process.env, FORCE_COLOR: '0' },
281
- stdio: ['ignore', 'pipe', 'pipe'],
282
- },
283
- );
284
- let output = '';
285
- child.stdout.setEncoding('utf8');
286
- child.stderr.setEncoding('utf8');
287
- const append = (chunk: string) => {
288
- output = (output + chunk).slice(0, 8_000);
289
- };
290
- child.stdout.on('data', append);
291
- child.stderr.on('data', append);
292
- child.on('error', (error) =>
293
- resolvePromise({ code: null, output: error.message }),
294
- );
295
- child.on('close', (code) => resolvePromise({ code, output }));
296
- });
297
- }
298
-
299
- async function listFiles(root: string, directory = root): Promise<string[]> {
300
- let entries;
301
- try {
302
- entries = await readdir(directory, { withFileTypes: true });
303
- } catch {
304
- return [];
305
- }
306
- const files: string[] = [];
307
- for (const entry of entries) {
308
- if (entry.name === 'node_modules') continue;
309
- const path = join(directory, entry.name);
310
- if (entry.isDirectory()) files.push(...(await listFiles(root, path)));
311
- else files.push(relative(root, path));
312
- }
313
- return files;
314
- }
315
-
316
- /* The scaffold refuses a target directory that holds anything but the
317
- specification, while a business manager may already have written
318
- translations there. Those files step aside for the scaffold and come back
319
- over the skeleton, so what the specialist wrote wins over the generated
320
- placeholder. */
321
- async function withScaffoldClearance<T>(
322
- modulePath: string,
323
- run: () => Promise<T>,
324
- ): Promise<T> {
325
- const extras = (await listFiles(modulePath)).filter(
326
- (file) => file !== 'spec/module.yaml',
327
- );
328
- if (extras.length === 0) return run();
329
- const stash = await mkdtemp(join(tmpdir(), 'flowdular-scaffold-'));
330
- try {
331
- for (const file of extras) {
332
- await cp(join(modulePath, file), join(stash, file));
333
- await rm(join(modulePath, file), { force: true });
334
- }
335
- for (const directory of await readdir(modulePath)) {
336
- if (directory === 'spec') continue;
337
- await rm(join(modulePath, directory), { recursive: true, force: true });
338
- }
339
- return await run();
340
- } finally {
341
- for (const file of extras) {
342
- await cp(join(stash, file), join(modulePath, file), { force: true });
343
- }
344
- await rm(stash, { recursive: true, force: true });
345
- }
346
- }
347
-
348
- /* A new module is created by the same capability the CLI exposes, from the
349
- specification the business manager wrote. The sandbox never hand-writes a
350
- module skeleton of its own, and it formats the skeleton right away so the
351
- format gate reports the specialist's files, not the generator's. */
352
- export async function scaffoldFromSpec(
353
- context: TurnContext,
354
- session: SandboxSession,
355
- ): Promise<string | null> {
356
- const paths = sessionPaths(
357
- context.workspaceRoot,
358
- session.id,
359
- session.moduleSuffix,
360
- );
361
- const notes: string[] = [];
362
- for (const module of session.modules) {
363
- if (module.kind !== 'new') continue;
364
- const modulePath = modulePathOf(paths, module.directory);
365
- const specPath = join(modulePath, 'spec', 'module.yaml');
366
- if (!(await exists(specPath))) continue;
367
- if (await exists(join(modulePath, 'module.json'))) continue;
368
- const spec = await readFile(specPath, 'utf8');
369
- if (!isSpecApproved(module, spec)) {
370
- notes.push(
371
- `The ${module.id} specification is not approved by the operator yet, so the module skeleton was not created.`,
372
- );
373
- continue;
374
- }
375
- const result = await withScaffoldClearance(modulePath, () =>
376
- runCli(context.workspaceRoot, [
377
- 'module',
378
- 'new',
379
- module.id,
380
- '--spec',
381
- join('modules', module.directory, 'spec', 'module.yaml'),
382
- '--apply',
383
- '--root',
384
- paths.workspace,
385
- '--json',
386
- ]),
387
- );
388
- if (result.code !== 0) {
389
- notes.push(
390
- `The module scaffold for ${module.id} failed (exit ${result.code ?? 'signal'}): ${result.output.slice(0, 1_500) || 'the command produced no output.'}`,
391
- );
392
- continue;
393
- }
394
- await formatDirectory(context.workspaceRoot, modulePath);
395
- notes.push(
396
- `Created the module skeleton for ${module.id} from its approved specification.`,
397
- );
398
- }
399
- return notes.length > 0 ? notes.join('\n') : null;
400
- }
401
-
402
- /* Diffs are recomputed only when a file under the base or the draft changed.
403
- The signature is one stat per file, the diff is an LCS per changed file. */
404
- interface DiffCacheEntry {
405
- readonly signature: string;
406
- readonly diffs: readonly SessionFileDiff[];
407
- }
408
- const diffCache = new Map<string, DiffCacheEntry>();
409
- const DIFF_CACHE_LIMIT = 64;
410
-
411
- async function treeSignature(
412
- hash: ReturnType<typeof createHash>,
413
- root: string,
414
- ): Promise<void> {
415
- for (const file of (await listFiles(root)).sort()) {
416
- try {
417
- const info = await stat(join(root, file));
418
- hash.update(`${file}\0${info.mtimeMs}\0${info.size}\n`);
419
- } catch {
420
- continue;
421
- }
422
- }
423
- }
424
-
425
- export async function collectDiffs(
426
- workspaceRoot: string,
427
- session: SandboxSession,
428
- ): Promise<readonly SessionFileDiff[]> {
429
- const paths = sessionPaths(workspaceRoot, session.id, session.moduleSuffix);
430
- const hash = createHash('sha1');
431
- for (const module of session.modules) {
432
- hash.update(`${module.directory}\n`);
433
- await treeSignature(hash, basePathOf(paths, module.directory));
434
- await treeSignature(hash, modulePathOf(paths, module.directory));
435
- }
436
- const signature = hash.digest('hex');
437
- const cached = diffCache.get(session.id);
438
- if (cached && cached.signature === signature) return cached.diffs;
439
-
440
- const diffs: SessionFileDiff[] = [];
441
- for (const module of session.modules) {
442
- for (const diff of await diffTrees(
443
- basePathOf(paths, module.directory),
444
- modulePathOf(paths, module.directory),
445
- )) {
446
- diffs.push({ ...diff, module: module.directory });
447
- }
448
- }
449
- if (diffCache.size >= DIFF_CACHE_LIMIT) {
450
- diffCache.delete(diffCache.keys().next().value!);
451
- }
452
- diffCache.set(session.id, { signature, diffs });
453
- return diffs;
454
- }
455
-
456
- export function forgetDiffs(sessionId: string): void {
457
- diffCache.delete(sessionId);
458
- }
459
-
460
- export async function runSessionGates(
461
- context: TurnContext,
462
- session: SandboxSession,
463
- gates: readonly string[],
464
- modules?: readonly SessionModule[],
465
- signal?: AbortSignal,
466
- ): Promise<readonly GateResult[]> {
467
- const selected = gates.filter(isGateId) as GateId[];
468
- if (selected.length === 0) return [];
469
- if (context.executeGates) {
470
- return context.executeGates({
471
- session,
472
- gates: selected,
473
- ...(modules ? { modules } : {}),
474
- });
475
- }
476
- return runGates({
477
- workspaceRoot: context.workspaceRoot,
478
- paths: sessionPaths(
479
- context.workspaceRoot,
480
- session.id,
481
- session.moduleSuffix,
482
- ),
483
- session,
484
- gates: selected,
485
- ...(modules ? { modules } : {}),
486
- ...(signal ? { signal } : {}),
487
- });
488
- }
489
-
490
- /* The dependency install is a gate in everything but name: when a declared
491
- package cannot be installed, the specialist must hear it as a failure with
492
- the installer's output, not as a typecheck error about a missing module. */
493
- async function installGate(
494
- context: TurnContext,
495
- session: SandboxSession,
496
- ): Promise<GateResult | null> {
497
- const install =
498
- context.installDependencies ??
499
- ((target: SandboxSession) =>
500
- installSessionDependencies(context.workspaceRoot, target));
501
- const result = await install(session);
502
- if (!result.ran) return null;
503
- return {
504
- id: 'dependencies',
505
- status: result.ok ? 'passed' : 'failed',
506
- durationMs: result.durationMs,
507
- command: 'pnpm install (session workspace)',
508
- output: result.ok
509
- ? `Installed the session workspace in ${result.durationMs} ms.`
510
- : `The session workspace could not install the declared dependencies:\n${result.output}`,
511
- };
512
- }
513
-
514
- /* A turn writes in one module, so the role's paths resolve against that
515
- module's directory and nowhere else. */
516
- function allowedPathsFor(
517
- role: AgentRoleDefinition,
518
- module: SessionModule,
519
- ): readonly string[] {
520
- return role.allowedPaths.map((path) => `modules/${module.directory}/${path}`);
521
- }
522
-
523
- function gateSummary(gate: GateResult): string {
524
- const label = gate.module ? `${gate.id} (modules/${gate.module})` : gate.id;
525
- return gate.status === 'failed'
526
- ? `Gate ${label} failed.\nCommand: ${gate.command}\n\n${gate.output}`
527
- : `Gate ${label}: ${gate.status}`;
528
- }
529
-
530
- /* One chat turn: the selected role drives the selected driver inside the
531
- session workspace, every event is appended to the durable chat log, and the
532
- diff plus the role's gates are collected when the driver finishes. */
533
- export async function* runTurn(
534
- context: TurnContext,
535
- input: TurnInput,
536
- ): AsyncGenerator<ChatEntry, TurnOutcome> {
537
- const session = await readSession(context.workspaceRoot, input.sessionId);
538
- const paths = sessionPaths(
539
- context.workspaceRoot,
540
- session.id,
541
- session.moduleSuffix,
542
- );
543
- const message = input.message.trim();
544
- if (message.length < 1 || message.length > 20_000) {
545
- throw new SandboxSetupError(
546
- 'INVALID_MESSAGE',
547
- 'A turn needs a message of 1 to 20000 characters.',
548
- );
549
- }
550
- const transcript = await readChat(context.workspaceRoot, session);
551
- const active = activeModule(session, input.module, transcript);
552
- const modulePath = modulePathOf(paths, active.directory);
553
- const gate = await readSpecGate(
554
- active,
555
- modulePath,
556
- basePathOf(paths, active.directory),
557
- );
558
- const requested = input.role ?? 'auto';
559
- const routed =
560
- requested === 'auto'
561
- ? routeRole(
562
- await routingContext(
563
- context,
564
- session,
565
- paths,
566
- modulePath,
567
- message,
568
- transcript,
569
- gate.approved,
570
- ),
571
- )
572
- : { role: requested, reason: '' };
573
- const roleId = routed.role;
574
- const role = findRole(context.roles, roleId);
575
- const driverId = input.driver ?? session.driver;
576
- const driver = await context.registry.resolve(driverId);
577
-
578
- const attachmentNote = attachmentInstruction(session.attachments);
579
-
580
- yield await appendChatEntry(context.workspaceRoot, session, {
581
- kind: 'user',
582
- role: roleId,
583
- module: active.directory,
584
- text: message,
585
- ...(session.attachments.length > 0
586
- ? { attachments: session.attachments }
587
- : {}),
588
- });
589
-
590
- if (refusesUnapproved(gate, roleId)) {
591
- return yield* refuseUnapproved(context, session, active, role, gate, {
592
- brief: session.brief || message,
593
- driver: driverId,
594
- });
595
- }
596
-
597
- await updateSession(context.workspaceRoot, session.id, {
598
- state: 'editing',
599
- role: roleId,
600
- driver: driverId,
601
- });
602
-
603
- if (routed.reason) {
604
- yield await appendChatEntry(context.workspaceRoot, session, {
605
- kind: 'system',
606
- role: roleId,
607
- module: active.directory,
608
- text: `Routed to ${role.name}. ${routed.reason}`,
609
- });
610
- }
611
-
612
- /* An approved specification produces the module skeleton before the turn
613
- starts, so an engineer never has to invent the module layout. */
614
- const preparation = await scaffoldFromSpec(context, session);
615
- if (preparation) {
616
- yield await appendChatEntry(context.workspaceRoot, session, {
617
- kind: 'system',
618
- role: roleId,
619
- text: preparation,
620
- });
621
- const install = await installGate(context, session);
622
- if (install && install.status === 'failed') {
623
- yield await appendChatEntry(context.workspaceRoot, session, {
624
- kind: 'system',
625
- role: roleId,
626
- text: gateSummary(install),
627
- });
628
- }
629
- }
630
-
631
- const skill = selectTaskSkill({
632
- role: role.id,
633
- sessionKind: active.kind === 'new' ? 'new-module' : 'edit-module',
634
- blueprint: session.blueprint,
635
- task: message,
636
- available: await listSkills(context.workspaceRoot),
637
- });
638
- const reviewing = skill === 'auto-review';
639
- if (reviewing) await prepareAutoReview(context.workspaceRoot, paths, active);
640
- const reviewRevision = reviewing
641
- ? await moduleReviewRevision(modulePath)
642
- : null;
643
- if (reviewing) await invalidateAutoReview(paths, active);
644
- let reviewPassed = false;
645
- const turnAllowedPaths = reviewing ? [] : allowedPathsFor(role, active);
646
- const team = context.roles.filter((mate) => role.handoff.includes(mate.id));
647
- const instruction = composeInstruction(role, {
648
- moduleId: active.id,
649
- modulePath: `modules/${active.directory}`,
650
- sessionKind: active.kind === 'new' ? 'new-module' : 'edit-module',
651
- blueprint: session.blueprint,
652
- allowedPaths: turnAllowedPaths,
653
- skill,
654
- team: team.map((mate) => `${mate.id}: ${mate.purpose}`),
655
- notes: [
656
- ...(reviewing
657
- ? [
658
- 'Read the complete active-module change against reference/auto-review-base/. This turn is read-only. Preserve the intended next-specialist handoff from the implementation turn after a passing review.',
659
- ]
660
- : []),
661
- ...(session.modules.length > 1
662
- ? [
663
- `This session works on several modules: ${session.modules
664
- .map(
665
- (module) =>
666
- `${module.id} (modules/${module.directory}, ${module.kind === 'new' ? 'new' : 'existing'})`,
667
- )
668
- .join(
669
- ', ',
670
- )}. This turn is yours in modules/${active.directory} only; another specialist takes the turn for the others. Each module is a project of this pnpm workspace, so a draft that imports another draft resolves the session copy.`,
671
- ]
672
- : []),
673
- 'reference/ is read-only. Consult only the code and references needed for this task; do not preload its catalog.',
674
- 'Other module.json files under modules/ describe the dependency graph. Only the draft module directories have sources you may change.',
675
- ...(active.kind === 'edit'
676
- ? [
677
- 'The module already exists. Read it before changing it and keep every existing behavior that the request does not ask you to change.',
678
- ]
679
- : []),
680
- ],
681
- });
682
- await writeAgentPointer(
683
- paths.workspace,
684
- driverId === 'claude-code' ? 'CLAUDE.md' : 'AGENTS.md',
685
- role.name,
686
- skill,
687
- );
688
-
689
- const history = historyFrom(await readChat(context.workspaceRoot, session));
690
- const resumeId = session.resumeIds[driverId] ?? null;
691
- let nextResumeId = resumeId;
692
- let failed = false;
693
- let closing = '';
694
- /* This snapshot is the enforcement point for role ownership. The workspace
695
- remains readable to the agent, but changes outside its module allowlist are
696
- quarantined and restored before any formatter, gate, checkpoint or delivery
697
- can observe them. */
698
- const pathGuard = await guardAgentPaths({
699
- workspace: paths.workspace,
700
- sessionRoot: paths.root,
701
- allowedPaths: turnAllowedPaths,
702
- });
703
-
704
- try {
705
- for await (const event of driver.run({
706
- workspacePath: paths.workspace,
707
- allowedPaths: turnAllowedPaths,
708
- role: roleId,
709
- systemInstruction: instruction,
710
- prompt: attachmentNote ? `${attachmentNote}\n\n${message}` : message,
711
- resumeId,
712
- history: history.slice(0, -1),
713
- model: session.model,
714
- signal: input.signal,
715
- })) {
716
- yield await appendChatEntry(context.workspaceRoot, session, {
717
- kind: eventKind(event),
718
- role: roleId,
719
- module: active.directory,
720
- ...(event.type === 'assistant.message' ? { text: event.text } : {}),
721
- event,
722
- });
723
- if (event.type === 'assistant.message') closing = event.text;
724
- if (event.type === 'turn.completed') {
725
- nextResumeId = event.resumeId ?? nextResumeId;
726
- failed = event.finishReason === 'error';
727
- }
728
- if (event.type === 'error') failed = true;
729
- }
730
- } catch (error) {
731
- failed = true;
732
- yield await appendChatEntry(context.workspaceRoot, session, {
733
- kind: 'system',
734
- role: roleId,
735
- module: active.directory,
736
- text:
737
- error instanceof Error
738
- ? `The coding agent stopped: ${error.message}`
739
- : 'The coding agent stopped unexpectedly.',
740
- event: {
741
- type: 'error',
742
- code: 'AGENT_TURN_FAILED',
743
- message: 'The coding agent stopped unexpectedly.',
744
- } as CodingAgentEvent,
745
- });
746
- }
747
-
748
- const pathResult = await pathGuard.verify();
749
- if (pathResult.violations.length > 0) {
750
- failed = true;
751
- const evidence = pathResult.violations
752
- .map((violation) => `- ${violation.path}: ${violation.reason}`)
753
- .join('\n');
754
- yield await appendChatEntry(context.workspaceRoot, session, {
755
- kind: 'system',
756
- role: roleId,
757
- module: active.directory,
758
- text: `This turn wrote outside its allowed paths and was failed. The sandbox restored those paths before validation.\n${evidence}${pathResult.quarantine ? `\nEvidence was quarantined at ${pathResult.quarantine}.` : ''}`,
759
- event: {
760
- type: 'error',
761
- code: 'ALLOWED_PATHS_VIOLATION',
762
- message: evidence.slice(0, 500),
763
- } as CodingAgentEvent,
764
- });
765
- }
766
-
767
- const scaffoldNote =
768
- failed || reviewing ? null : await scaffoldFromSpec(context, session);
769
- if (scaffoldNote) {
770
- yield await appendChatEntry(context.workspaceRoot, session, {
771
- kind: 'system',
772
- role: roleId,
773
- text: scaffoldNote,
774
- });
775
- }
776
-
777
- /* Prettier is deterministic and not a correctness signal, so the sandbox
778
- formats the draft modules itself after each turn rather than bouncing the
779
- format gate back to the agent over whitespace. */
780
- if (!failed && !reviewing) {
781
- const turnPaths = sessionPaths(
782
- context.workspaceRoot,
783
- session.id,
784
- session.moduleSuffix,
785
- );
786
- for (const module of session.modules) {
787
- await formatDirectory(
788
- context.workspaceRoot,
789
- modulePathOf(turnPaths, module.directory),
790
- );
791
- }
792
- }
793
-
794
- const diffs = await collectDiffs(context.workspaceRoot, session);
795
- const gates: GateResult[] = [];
796
- if ((diffs.length > 0 || reviewing) && !failed) {
797
- /* Only the modules that hold changes are gated: a module nobody touched
798
- has nothing to check and its gates would cost minutes for no signal. */
799
- const changed = new Set(diffs.map((diff) => diff.module));
800
- const gated = session.modules.filter((module) =>
801
- changed.has(module.directory),
802
- );
803
- /* The install runs whenever a package.json changed and counts as the
804
- dependencies gate; the role's own gates follow, plus the declared
805
- dependency check the session cannot do without. */
806
- const install = await installGate(context, session);
807
- if (install) gates.push(install);
808
- if (!install || install.status !== 'failed') {
809
- gates.push(
810
- ...(await runSessionGates(
811
- context,
812
- session,
813
- [
814
- ...(reviewing
815
- ? GATE_IDS.filter((id) => id !== 'auto-review')
816
- : role.gates),
817
- ...(role.gates.includes('dependencies') ? [] : ['dependencies']),
818
- ],
819
- reviewing ? [active] : gated,
820
- input.signal,
821
- )),
822
- );
823
- }
824
- }
825
- if (
826
- reviewing &&
827
- !failed &&
828
- !input.signal?.aborted &&
829
- reviewRevision !== null
830
- ) {
831
- const verified = GATE_IDS.filter((id) => id !== 'auto-review').every((id) =>
832
- gates.some(
833
- (gate) =>
834
- gate.id === id &&
835
- gate.status === 'passed' &&
836
- (gate.module === active.directory ||
837
- ((id === 'spec-schema' || id === 'module-schema') &&
838
- gate.module === undefined)),
839
- ),
840
- );
841
- if (verified && gates.every((gate) => gate.status === 'passed')) {
842
- reviewPassed = await recordAutoReview(
843
- paths,
844
- active,
845
- reviewRevision,
846
- closing,
847
- );
848
- }
849
- gates.push(
850
- ...(await runSessionGates(
851
- context,
852
- session,
853
- ['auto-review'],
854
- [active],
855
- input.signal,
856
- )),
857
- );
858
- }
859
-
860
- if (reviewing && !reviewPassed && !failed) {
861
- const index = gates.findIndex(
862
- (gate) => gate.id === 'auto-review' && gate.status === 'failed',
863
- );
864
- if (index >= 0 && /"verdict"\s*:\s*"fail"/.test(closing)) {
865
- gates[index] = {
866
- ...gates[index]!,
867
- output:
868
- 'Use $module-update to fix the findings in your preceding review. Preserve unrelated behavior, add regression tests, and finish implementation before requesting another review.',
869
- };
870
- }
871
- }
872
- const declared: HandoffDeclaration | null = closing
873
- ? parseHandoff(closing)
874
- : null;
875
- /* The turn may have written the specification, so the gate is read again:
876
- what the next role may do depends on the document as it is now. */
877
- const closingGate = await readSpecGate(
878
- active,
879
- modulePath,
880
- basePathOf(paths, active.directory),
881
- );
882
- const handoffContext = {
883
- routing: await routingContext(
884
- context,
885
- session,
886
- paths,
887
- modulePath,
888
- message,
889
- [],
890
- closingGate.approved,
891
- ),
892
- role: roleId,
893
- module: active.directory,
894
- declared,
895
- gates,
896
- failed,
897
- changed: diffs.length > 0,
898
- specApproved: closingGate.approved,
899
- brief: session.brief || message,
900
- reviewing,
901
- };
902
- let handoff = planHandoff(handoffContext);
903
- if (handoff.kind === 'review') {
904
- // Let intermediate specialists finish their handoffs before a full review.
905
- // Every delivered module, including unchanged drafts, needs a current record.
906
- const remaining = session.modules.filter(
907
- (module) =>
908
- !gates.some(
909
- (gate) =>
910
- gate.id === 'auto-review' && gate.module === module.directory,
911
- ),
912
- );
913
- if (remaining.length > 0) {
914
- gates.push(
915
- ...(await runSessionGates(
916
- context,
917
- session,
918
- ['auto-review'],
919
- remaining,
920
- input.signal,
921
- )),
922
- );
923
- handoff = planHandoff(handoffContext);
924
- }
925
- }
926
-
927
- for (const gate of gates) {
928
- yield await appendChatEntry(context.workspaceRoot, session, {
929
- kind: 'system',
930
- role: roleId,
931
- ...(gate.module ? { module: gate.module } : {}),
932
- text: gateSummary(gate),
933
- event: {
934
- type: gate.status === 'failed' ? 'error' : 'tool.completed',
935
- ...(gate.status === 'failed'
936
- ? {
937
- code: `GATE_${gate.id.toUpperCase()}`,
938
- message: gate.output.slice(0, 500),
939
- }
940
- : { tool: `gate:${gate.id}`, detail: gate.command, ok: true }),
941
- } as CodingAgentEvent,
942
- });
943
- }
944
-
945
- const handoffEntry = await appendChatEntry(context.workspaceRoot, session, {
946
- kind: 'system',
947
- role: handoff.role,
948
- ...(handoff.module ? { module: handoff.module } : {}),
949
- text: handoffText(handoff),
950
- handoff,
951
- });
952
- yield handoffEntry;
953
-
954
- /* A turn that changed files becomes a restore point, keyed by the handoff
955
- entry so the transcript line and its checkpoint share one sequence. */
956
- if (diffs.length > 0) {
957
- await captureCheckpoint(
958
- context.workspaceRoot,
959
- session,
960
- handoffEntry.sequence,
961
- { label: role.name, role: roleId },
962
- );
963
- }
964
-
965
- const updated = await updateSession(context.workspaceRoot, session.id, {
966
- resumeIds: nextResumeId
967
- ? { ...session.resumeIds, [driverId]: nextResumeId }
968
- : session.resumeIds,
969
- state: failed
970
- ? 'failed'
971
- : handoff.kind === 'approval' || handoff.kind === 'question'
972
- ? 'awaiting-approval'
973
- : gates.some((gate) => gate.status !== 'passed')
974
- ? 'validating'
975
- : diffs.length > 0
976
- ? 'previewing'
977
- : 'planned',
978
- role: roleId,
979
- driver: driverId,
980
- });
981
-
982
- if (context.platform && updated.registeredWithPlatform) {
983
- await context.platform
984
- .updateSessionState(updated.id, updated.state)
985
- .catch(() => undefined);
986
- }
987
-
988
- return { session: updated, diffs, gates, handoff };
989
- }
990
-
991
- function eventKind(event: CodingAgentEvent): ChatEntry['kind'] {
992
- return event.type === 'assistant.message' ? 'agent' : 'event';
993
- }