@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,849 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import {
3
- access,
4
- appendFile,
5
- cp,
6
- mkdir,
7
- readFile,
8
- readdir,
9
- realpath,
10
- rename,
11
- rm,
12
- writeFile,
13
- } from 'node:fs/promises';
14
- import { isAbsolute, join, relative, resolve } from 'node:path';
15
- import type { CodingAgentEvent } from '@flowdular/sdk/coding-agent';
16
- import { sandboxDirectory } from './config.ts';
17
- import { materializeModuleGraph, materializeReference } from './reference.ts';
18
- import { hashSpec } from './spec.ts';
19
- import { forgetDiffs } from './turns.ts';
20
- import {
21
- ensureSessionDependencies,
22
- materializeSessionWorkspace,
23
- type InstallResult,
24
- } from './workspace-install.ts';
25
- import { SandboxSetupError } from './workspace-root.ts';
26
-
27
- export type SandboxSessionKind = 'new-module' | 'edit-module';
28
-
29
- export type SandboxSessionState =
30
- | 'draft'
31
- | 'classified'
32
- | 'planned'
33
- | 'editing'
34
- | 'validating'
35
- | 'previewing'
36
- | 'awaiting-approval'
37
- | 'accepted'
38
- | 'failed'
39
- | 'blocked'
40
- | 'deleted';
41
-
42
- /* What happens after a turn. The orchestrator writes one of these at the end of
43
- every turn, so the session always says who continues and why. */
44
- export interface HandoffPlan {
45
- readonly kind: 'continue' | 'approval' | 'question' | 'review' | 'blocked';
46
- readonly role: string;
47
- readonly roleName: string;
48
- readonly reason: string;
49
- readonly prompt: string;
50
- /* The draft module directory the next turn works in. Absent on handoffs
51
- written before a session could target one module of several. */
52
- readonly module?: string;
53
- }
54
-
55
- /* One draft module of a session. The directory is the module directory under
56
- modules/ in both the session workspace and, for an edit, the host
57
- workspace it was copied from. */
58
- export interface SessionModule {
59
- readonly id: string;
60
- readonly directory: string;
61
- readonly kind: 'new' | 'edit';
62
- /* When the operator approved this module's specification, and the hash of
63
- the exact text they approved. A later edit changes the hash, so the
64
- approval belongs to that version of the document and nothing else. */
65
- readonly specApprovedAt?: number;
66
- readonly specHash?: string;
67
- }
68
-
69
- /* A file the operator attached to the session to show what they want changed.
70
- The bytes live under the session directory; the same file is copied into the
71
- workspace so the coding agent, which may only read inside the workspace, can
72
- open it. `name` is the sanitised filename and doubles as the workspace copy's
73
- name, so it is unique within the session. */
74
- export interface SessionAttachment {
75
- readonly id: string;
76
- readonly name: string;
77
- readonly kind: 'image' | 'file';
78
- readonly size: number;
79
- readonly addedAt: number;
80
- }
81
-
82
- /* A restore point the operator can roll the workspace back to. The snapshot of
83
- each draft module tree lives under checkpoints/<sequence>/; the metadata here
84
- says which turn produced it. `sequence` is the chat entry it belongs to, so
85
- the transcript line and its restore point share one identity. */
86
- export interface SessionCheckpoint {
87
- readonly sequence: number;
88
- readonly at: number;
89
- readonly label: string;
90
- readonly role: string;
91
- }
92
-
93
- export interface SandboxSession {
94
- /* Historical records have no owner. Never infer one from the next login. */
95
- readonly owner?: SessionOwner;
96
- readonly rejectedAt?: number | null;
97
- readonly id: string;
98
- readonly kind: SandboxSessionKind;
99
- /* The primary module: modules[0]. Kept as fields for the current UI. */
100
- readonly moduleId: string;
101
- readonly moduleSuffix: string;
102
- readonly modules: readonly SessionModule[];
103
- readonly title: string;
104
- /* The request the session started from, replayed to every specialist that
105
- picks the work up later. */
106
- readonly brief: string;
107
- readonly blueprint: string;
108
- readonly role: string;
109
- readonly driver: string;
110
- readonly model: string | null;
111
- readonly resumeIds: Readonly<Record<string, string>>;
112
- /* When true, the orchestrator starts the handed-off turn without waiting for
113
- the operator. Approval and failure always stop, whatever this says. */
114
- readonly autoContinue: boolean;
115
- /* Automatic turns run since the operator last spoke. */
116
- readonly chainDepth: number;
117
- /* Files the operator attached to show the desired change. Bounded per
118
- session; see attachments.ts for the limits. */
119
- readonly attachments: readonly SessionAttachment[];
120
- /* Restore points, oldest first. Bounded; see checkpoints.ts for the cap and
121
- the pruning that never drops the start. */
122
- readonly checkpoints: readonly SessionCheckpoint[];
123
- readonly state: SandboxSessionState;
124
- readonly createdAt: number;
125
- readonly updatedAt: number;
126
- readonly ejectedAt: number | null;
127
- readonly archivedAt: number | null;
128
- readonly registeredWithPlatform: boolean;
129
- }
130
-
131
- export interface SessionOwner {
132
- readonly platformUrl: string;
133
- readonly tenantId: string;
134
- readonly accountId: string;
135
- }
136
-
137
- export interface ChatEntry {
138
- readonly decision?: 'approved' | 'changes-requested';
139
- readonly sequence: number;
140
- readonly at: number;
141
- readonly kind: 'user' | 'agent' | 'event' | 'system';
142
- readonly role: string;
143
- /* The draft module directory the entry belongs to. Absent on entries that
144
- are about the session itself, and on entries written before a session
145
- could carry several modules. */
146
- readonly module?: string;
147
- readonly text?: string;
148
- readonly event?: CodingAgentEvent;
149
- readonly handoff?: HandoffPlan;
150
- /* Attachments included with this turn, echoed onto the user entry so the
151
- transcript records exactly what the agent was shown. */
152
- readonly attachments?: readonly SessionAttachment[];
153
- }
154
-
155
- export interface SessionPaths {
156
- readonly root: string;
157
- readonly workspace: string;
158
- /* The primary module directory. */
159
- readonly modulePath: string;
160
- readonly base: string;
161
- readonly data: string;
162
- /* Where module-tree snapshots live, one directory per checkpoint sequence. */
163
- readonly checkpoints: string;
164
- readonly chatLog: string;
165
- readonly record: string;
166
- /* Where attachment bytes are stored, and the workspace copy the coding
167
- agent reads from. */
168
- readonly attachments: string;
169
- readonly workspaceAttachments: string;
170
- }
171
-
172
- const SESSION_ID =
173
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
174
-
175
- export function isSessionId(value: string): boolean {
176
- return SESSION_ID.test(value);
177
- }
178
-
179
- /* Session ids are the only client-chosen segment that reaches the file
180
- system, so anything that is not an id the sandbox itself minted is refused
181
- before a path is built from it. */
182
- export function assertSessionId(value: string): string {
183
- if (!isSessionId(value)) {
184
- throw new SandboxSetupError(
185
- 'INVALID_SESSION_ID',
186
- 'The session id is not a sandbox session identifier.',
187
- );
188
- }
189
- return value;
190
- }
191
-
192
- export function moduleSuffixOf(moduleId: string): string {
193
- const parts = moduleId.split('.');
194
- return (parts.at(-1) === 'core' ? parts.slice(0, -1) : parts).join('-');
195
- }
196
-
197
- function sessionsRoot(workspaceRoot: string): string {
198
- return join(sandboxDirectory(workspaceRoot), 'sessions');
199
- }
200
-
201
- function sessionRoot(workspaceRoot: string, sessionId: string): string {
202
- const parent = sessionsRoot(workspaceRoot);
203
- const root = resolve(parent, sessionId);
204
- const inside = relative(parent, root);
205
- if (!inside || inside.startsWith('..') || isAbsolute(inside)) {
206
- throw new SandboxSetupError(
207
- 'INVALID_SESSION_ID',
208
- 'The session id is not a sandbox session identifier.',
209
- );
210
- }
211
- return root;
212
- }
213
-
214
- export function sessionPaths(
215
- workspaceRoot: string,
216
- sessionId: string,
217
- moduleSuffix: string,
218
- ): SessionPaths {
219
- const root = sessionRoot(workspaceRoot, sessionId);
220
- const workspace = join(root, 'workspace');
221
- return {
222
- root,
223
- workspace,
224
- modulePath: join(workspace, 'modules', moduleSuffix),
225
- base: join(root, 'base'),
226
- data: join(root, 'data'),
227
- checkpoints: join(root, 'checkpoints'),
228
- chatLog: join(root, 'chat.jsonl'),
229
- record: join(root, 'session.json'),
230
- attachments: join(root, 'attachments'),
231
- workspaceAttachments: join(workspace, 'reference', 'attachments'),
232
- };
233
- }
234
-
235
- export function modulePathOf(paths: SessionPaths, directory: string): string {
236
- return join(paths.workspace, 'modules', directory);
237
- }
238
-
239
- export function basePathOf(paths: SessionPaths, directory: string): string {
240
- return join(paths.base, 'modules', directory);
241
- }
242
-
243
- export function checkpointModulePath(
244
- paths: SessionPaths,
245
- sequence: number,
246
- directory: string,
247
- ): string {
248
- return join(paths.checkpoints, String(sequence), 'modules', directory);
249
- }
250
-
251
- async function exists(path: string): Promise<boolean> {
252
- try {
253
- await access(path);
254
- return true;
255
- } catch {
256
- return false;
257
- }
258
- }
259
-
260
- const COPY_EXCLUDED = new Set(['node_modules', 'dist', '.turbo']);
261
-
262
- export async function copyModuleTree(
263
- source: string,
264
- target: string,
265
- ): Promise<void> {
266
- await cp(source, target, {
267
- recursive: true,
268
- filter: (path) => {
269
- const segments = path.split('/');
270
- return !segments.some((segment) => COPY_EXCLUDED.has(segment));
271
- },
272
- });
273
- }
274
-
275
- /* A session workspace is a Flowdular workspace of its own: the draft modules,
276
- the manifests of every other enabled module, the shared TypeScript and
277
- formatting contracts, and a pnpm workspace that links the live framework
278
- packages so gates run against the code the platform runs. */
279
- async function prepareSessionWorkspace(options: {
280
- readonly workspaceRoot: string;
281
- readonly paths: SessionPaths;
282
- readonly modules: readonly SessionModule[];
283
- }): Promise<void> {
284
- await mkdir(join(options.paths.workspace, 'modules'), { recursive: true });
285
- await mkdir(options.paths.data, { recursive: true });
286
-
287
- const graph = await materializeModuleGraph(
288
- options.workspaceRoot,
289
- options.paths.workspace,
290
- options.modules.map((module) => module.id),
291
- );
292
- await materializeReference(options.workspaceRoot, options.paths.workspace);
293
- await writeFile(
294
- join(options.paths.workspace, 'flowdular.json'),
295
- `${JSON.stringify(
296
- {
297
- schemaVersion: 1,
298
- architectureVersion: '0.2.0',
299
- specs: { platformRoot: 'specs', moduleDirectory: 'spec' },
300
- modules: {
301
- roots: ['modules'],
302
- enabled: [
303
- ...graph,
304
- ...options.modules.map((module) => module.id),
305
- ].sort(),
306
- },
307
- locales: ['en', 'pl'],
308
- },
309
- null,
310
- '\t',
311
- )}\n`,
312
- 'utf8',
313
- );
314
- for (const shared of ['tsconfig.base.json', '.prettierrc.json']) {
315
- await cp(
316
- join(options.workspaceRoot, shared),
317
- join(options.paths.workspace, shared),
318
- ).catch(() => undefined);
319
- }
320
- await mkdir(join(options.paths.workspace, 'specs'), { recursive: true });
321
- await materializeSessionWorkspace({
322
- workspaceRoot: options.workspaceRoot,
323
- sessionWorkspace: options.paths.workspace,
324
- modules: options.modules,
325
- });
326
- }
327
-
328
- /* Installs the draft modules' dependencies when they changed. The result is
329
- what the caller reports: a failed install is a gate failure the agent has to
330
- see, not a silent typecheck error. */
331
- export function installSessionDependencies(
332
- workspaceRoot: string,
333
- session: SandboxSession,
334
- force = false,
335
- ): Promise<InstallResult> {
336
- const paths = sessionPaths(workspaceRoot, session.id, session.moduleSuffix);
337
- return ensureSessionDependencies({
338
- sessionRoot: paths.root,
339
- sessionWorkspace: paths.workspace,
340
- modules: session.modules,
341
- force,
342
- });
343
- }
344
-
345
- export interface CreateSessionInput {
346
- readonly owner?: SessionOwner;
347
- readonly workspaceRoot: string;
348
- readonly kind: SandboxSessionKind;
349
- readonly moduleId: string;
350
- readonly title: string;
351
- readonly brief: string;
352
- readonly blueprint: string;
353
- readonly role: string;
354
- readonly driver: string;
355
- readonly model?: string | null;
356
- /* Module directory name inside the workspace, for an edit session. */
357
- readonly sourceModule?: string;
358
- /* Every module of the session; the first one is primary. Defaults to the
359
- single module named by moduleId. */
360
- readonly modules?: readonly SessionModule[];
361
- /* Run the dependency install right away. The routes run it themselves so
362
- they can report a failure into the transcript. */
363
- readonly install?: boolean;
364
- }
365
-
366
- const MODULE_ID = /^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/;
367
- const MODULE_DIRECTORY = /^[a-z][a-z0-9-]*$/;
368
-
369
- function assertModule(module: SessionModule): void {
370
- if (!MODULE_ID.test(module.id)) {
371
- throw new SandboxSetupError(
372
- 'INVALID_MODULE_ID',
373
- 'Module id must use lowercase dot-separated segments, for example sales.orders.',
374
- );
375
- }
376
- if (!MODULE_DIRECTORY.test(module.directory)) {
377
- throw new SandboxSetupError(
378
- 'INVALID_MODULE_ID',
379
- `Module directory ${module.directory} is not a module directory name.`,
380
- );
381
- }
382
- }
383
-
384
- export async function createSession(
385
- input: CreateSessionInput,
386
- ): Promise<SandboxSession> {
387
- const moduleSuffix = moduleSuffixOf(input.moduleId);
388
- const modules: readonly SessionModule[] = input.modules ?? [
389
- {
390
- id: input.moduleId,
391
- directory:
392
- input.kind === 'edit-module'
393
- ? (input.sourceModule ?? moduleSuffix)
394
- : moduleSuffix,
395
- kind: input.kind === 'edit-module' ? 'edit' : 'new',
396
- },
397
- ];
398
- if (modules.length === 0 || modules[0]!.id !== input.moduleId) {
399
- throw new SandboxSetupError(
400
- 'INVALID_MODULE_ID',
401
- 'The primary module must be the first module of the session.',
402
- );
403
- }
404
- for (const module of modules) assertModule(module);
405
- const primaryDirectory = modules[0]!.directory;
406
- const id = randomUUID();
407
- const paths = sessionPaths(input.workspaceRoot, id, primaryDirectory);
408
-
409
- for (const module of modules) {
410
- const target = modulePathOf(paths, module.directory);
411
- const base = basePathOf(paths, module.directory);
412
- if (module.kind === 'edit') {
413
- const source = join(input.workspaceRoot, 'modules', module.directory);
414
- await copyModuleTree(source, target);
415
- await copyModuleTree(source, base);
416
- } else {
417
- await mkdir(target, { recursive: true });
418
- await mkdir(base, { recursive: true });
419
- }
420
- }
421
-
422
- await prepareSessionWorkspace({
423
- workspaceRoot: input.workspaceRoot,
424
- paths,
425
- modules,
426
- });
427
-
428
- /* The pristine state is the first restore point, so an operator can always
429
- roll back to before any agent touched the module. */
430
- for (const module of modules) {
431
- await copyModuleTree(
432
- modulePathOf(paths, module.directory),
433
- checkpointModulePath(paths, 0, module.directory),
434
- );
435
- }
436
-
437
- const now = Date.now();
438
- const session: SandboxSession = {
439
- ...(input.owner ? { owner: input.owner } : {}),
440
- id,
441
- kind: input.kind,
442
- moduleId: input.moduleId,
443
- moduleSuffix: primaryDirectory,
444
- modules,
445
- title: input.title,
446
- brief: input.brief,
447
- blueprint: input.blueprint,
448
- role: input.role,
449
- driver: input.driver,
450
- model: input.model ?? null,
451
- resumeIds: {},
452
- autoContinue: true,
453
- chainDepth: 0,
454
- attachments: [],
455
- checkpoints: [
456
- { sequence: 0, at: now, label: 'the starting point', role: input.role },
457
- ],
458
- state: 'draft',
459
- createdAt: now,
460
- updatedAt: now,
461
- ejectedAt: null,
462
- archivedAt: null,
463
- registeredWithPlatform: false,
464
- };
465
- await writeSession(input.workspaceRoot, session);
466
- if (input.install === true) {
467
- await installSessionDependencies(input.workspaceRoot, session);
468
- }
469
- return session;
470
- }
471
-
472
- /* A module joins a session after it started: it is materialized into the
473
- workspace (with its pristine base, for an edit), appended to modules[], and
474
- the workspace manifests are regenerated so the new draft is a project of the
475
- session's pnpm workspace instead of a link to the host checkout. The caller
476
- installs afterwards; the install only runs when the new package.json changed
477
- the session's dependency signature. */
478
- export async function addSessionModule(
479
- workspaceRoot: string,
480
- session: SandboxSession,
481
- module: SessionModule,
482
- ): Promise<SandboxSession> {
483
- assertModule(module);
484
- if (
485
- session.modules.some(
486
- (entry) => entry.id === module.id || entry.directory === module.directory,
487
- )
488
- ) {
489
- throw new SandboxSetupError(
490
- 'MODULE_ALREADY_IN_SESSION',
491
- `${module.id} is already part of this session.`,
492
- );
493
- }
494
- const paths = sessionPaths(workspaceRoot, session.id, session.moduleSuffix);
495
- const target = modulePathOf(paths, module.directory);
496
- const base = basePathOf(paths, module.directory);
497
- if (module.kind === 'edit') {
498
- const source = join(workspaceRoot, 'modules', module.directory);
499
- await copyModuleTree(source, target);
500
- await copyModuleTree(source, base);
501
- } else {
502
- await mkdir(target, { recursive: true });
503
- await mkdir(base, { recursive: true });
504
- }
505
- const modules = [...session.modules, module];
506
- await prepareSessionWorkspace({ workspaceRoot, paths, modules });
507
- return updateSession(workspaceRoot, session.id, {
508
- modules,
509
- moduleId: modules[0]!.id,
510
- moduleSuffix: modules[0]!.directory,
511
- });
512
- }
513
-
514
- /* The record is replaced by rename, so a reader racing a running turn never
515
- sees a half-written file. */
516
- export async function writeSession(
517
- workspaceRoot: string,
518
- session: SandboxSession,
519
- ): Promise<SandboxSession> {
520
- const paths = sessionPaths(workspaceRoot, session.id, session.moduleSuffix);
521
- await mkdir(paths.root, { recursive: true });
522
- const staging = `${paths.record}.${process.pid}.${randomUUID().slice(0, 8)}`;
523
- await writeFile(staging, `${JSON.stringify(session, null, '\t')}\n`, 'utf8');
524
- await rename(staging, paths.record);
525
- return session;
526
- }
527
-
528
- export async function readSession(
529
- workspaceRoot: string,
530
- sessionId: string,
531
- ): Promise<SandboxSession> {
532
- const root = sessionRoot(workspaceRoot, assertSessionId(sessionId));
533
- try {
534
- const record = JSON.parse(
535
- await readFile(join(root, 'session.json'), 'utf8'),
536
- ) as SandboxSession;
537
- /* Sessions written before a field existed stay readable, so an older
538
- session opens instead of disappearing from the list. */
539
- return {
540
- ...record,
541
- brief: record.brief ?? '',
542
- modules: record.modules ?? [
543
- {
544
- id: record.moduleId,
545
- directory: record.moduleSuffix,
546
- kind: record.kind === 'edit-module' ? 'edit' : 'new',
547
- },
548
- ],
549
- autoContinue: record.autoContinue !== false,
550
- chainDepth: record.chainDepth ?? 0,
551
- attachments: record.attachments ?? [],
552
- checkpoints: record.checkpoints ?? [],
553
- ejectedAt: record.ejectedAt ?? null,
554
- archivedAt: record.archivedAt ?? null,
555
- };
556
- } catch (error) {
557
- if (error instanceof SandboxSetupError) throw error;
558
- throw new SandboxSetupError(
559
- 'SESSION_NOT_FOUND',
560
- `No sandbox session ${sessionId} exists in this workspace.`,
561
- );
562
- }
563
- }
564
-
565
- /* Every session that still has a record, archived ones included, so the
566
- client decides what to show. Deleted sessions are tombstones kept for their
567
- transcript and never listed. */
568
- export async function listSessions(
569
- workspaceRoot: string,
570
- includeDeleted = false,
571
- ): Promise<readonly SandboxSession[]> {
572
- let entries: readonly string[];
573
- try {
574
- entries = await readdir(sessionsRoot(workspaceRoot));
575
- } catch {
576
- return [];
577
- }
578
- const sessions: SandboxSession[] = [];
579
- for (const entry of entries) {
580
- if (!isSessionId(entry)) continue;
581
- try {
582
- const session = await readSession(workspaceRoot, entry);
583
- if (includeDeleted || session.state !== 'deleted') sessions.push(session);
584
- } catch {
585
- continue;
586
- }
587
- }
588
- return sessions.sort((left, right) => right.updatedAt - left.updatedAt);
589
- }
590
-
591
- export async function updateSession(
592
- workspaceRoot: string,
593
- sessionId: string,
594
- patch: Partial<Omit<SandboxSession, 'id' | 'createdAt'>>,
595
- ): Promise<SandboxSession> {
596
- const current = await readSession(workspaceRoot, sessionId);
597
- return writeSession(workspaceRoot, {
598
- ...current,
599
- ...patch,
600
- updatedAt: Date.now(),
601
- });
602
- }
603
-
604
- export function archiveSession(
605
- workspaceRoot: string,
606
- sessionId: string,
607
- ): Promise<SandboxSession> {
608
- return updateSession(workspaceRoot, sessionId, { archivedAt: Date.now() });
609
- }
610
-
611
- export function restoreSession(
612
- workspaceRoot: string,
613
- sessionId: string,
614
- ): Promise<SandboxSession> {
615
- return updateSession(workspaceRoot, sessionId, {
616
- archivedAt: null,
617
- rejectedAt: null,
618
- });
619
- }
620
-
621
- /* Replaces a draft module directory with a checkpoint snapshot: the current
622
- contents step aside, then the snapshot is copied over the emptied directory.
623
- node_modules stays so the session's install survives the rollback. */
624
- async function replaceModuleContents(
625
- target: string,
626
- snapshot: string,
627
- ): Promise<void> {
628
- await mkdir(target, { recursive: true });
629
- for (const entry of await readdir(target)) {
630
- if (entry === 'node_modules') continue;
631
- await rm(join(target, entry), { recursive: true, force: true });
632
- }
633
- await copyModuleTree(snapshot, target);
634
- }
635
-
636
- /* Rolls the session workspace back to a captured checkpoint. The transcript is
637
- append-only: the rollback is recorded as a marker, never by rewriting the
638
- history, so the record of what the agents did stays intact. */
639
- export async function restoreCheckpoint(
640
- workspaceRoot: string,
641
- sessionId: string,
642
- sequence: number,
643
- ): Promise<SandboxSession> {
644
- const session = await readSession(workspaceRoot, assertSessionId(sessionId));
645
- const checkpoint = session.checkpoints.find(
646
- (entry) => entry.sequence === sequence,
647
- );
648
- if (!checkpoint) {
649
- throw new SandboxSetupError(
650
- 'CHECKPOINT_NOT_FOUND',
651
- `No checkpoint for turn ${sequence} exists in this session.`,
652
- );
653
- }
654
- const paths = sessionPaths(workspaceRoot, session.id, session.moduleSuffix);
655
- for (const module of session.modules) {
656
- const snapshot = checkpointModulePath(paths, sequence, module.directory);
657
- /* A checkpoint older than the module says nothing about it, so that
658
- module keeps its current files instead of failing the rollback. */
659
- if (!(await exists(snapshot))) continue;
660
- await replaceModuleContents(
661
- modulePathOf(paths, module.directory),
662
- snapshot,
663
- );
664
- }
665
- forgetDiffs(session.id);
666
- await appendChatEntry(workspaceRoot, session, {
667
- kind: 'system',
668
- role: checkpoint.role,
669
- text: `Restored the workspace to the state after ${checkpoint.label} (turn ${sequence}).`,
670
- });
671
- return updateSession(workspaceRoot, session.id, { state: 'editing' });
672
- }
673
-
674
- export interface DeleteSessionOptions {
675
- /* Keep session.json and chat.jsonl as a tombstone. Default true. */
676
- readonly keepTranscript?: boolean;
677
- }
678
-
679
- /* Deleting removes what the session materialized: the workspace, the base
680
- copy and the ephemeral data. The record and the transcript stay by default,
681
- because they are the evidence of what the agents did. */
682
- export async function deleteSession(
683
- workspaceRoot: string,
684
- sessionId: string,
685
- options: DeleteSessionOptions = {},
686
- ): Promise<void> {
687
- const session = await readSession(workspaceRoot, sessionId);
688
- const paths = sessionPaths(workspaceRoot, session.id, session.moduleSuffix);
689
- /* A symlink planted inside the sessions directory must not turn the
690
- removal into one outside it. */
691
- const canonicalParent = await realpath(sessionsRoot(workspaceRoot));
692
- const canonicalRoot = await realpath(paths.root);
693
- const inside = relative(canonicalParent, canonicalRoot);
694
- if (!inside || inside.startsWith('..') || isAbsolute(inside)) {
695
- throw new SandboxSetupError(
696
- 'INVALID_SESSION_ID',
697
- 'The session directory is not inside the sandbox sessions directory.',
698
- );
699
- }
700
- lastSequence.delete(session.id);
701
- if (options.keepTranscript === false) {
702
- await rm(paths.root, { recursive: true, force: true });
703
- return;
704
- }
705
- for (const directory of [
706
- paths.workspace,
707
- paths.base,
708
- paths.data,
709
- paths.attachments,
710
- paths.checkpoints,
711
- ]) {
712
- await rm(directory, { recursive: true, force: true });
713
- }
714
- await writeSession(workspaceRoot, {
715
- ...session,
716
- state: 'deleted',
717
- updatedAt: Date.now(),
718
- });
719
- }
720
-
721
- /* Approving a draft specification is a one-line, reviewable edit: only the
722
- status field moves, so the operator's decision cannot rewrite the document.
723
- The hash of the approved text is recorded on the module, which is what makes
724
- the approval belong to this version of the specification and not to the file
725
- name: an edit after it re-opens the gate. */
726
- export async function approveSpecification(
727
- workspaceRoot: string,
728
- session: SandboxSession,
729
- module: SessionModule = session.modules[0]!,
730
- ): Promise<{
731
- readonly session: SandboxSession;
732
- readonly status: string;
733
- readonly module: string;
734
- }> {
735
- const paths = sessionPaths(workspaceRoot, session.id, session.moduleSuffix);
736
- const specPath = join(
737
- modulePathOf(paths, module.directory),
738
- 'spec',
739
- 'module.yaml',
740
- );
741
- let spec: string;
742
- try {
743
- spec = await readFile(specPath, 'utf8');
744
- } catch {
745
- throw new SandboxSetupError(
746
- 'SPEC_NOT_FOUND',
747
- `${module.id} has no specification to approve yet.`,
748
- );
749
- }
750
- const status = /^status:[ \t]*(\S+)[ \t]*$/m.exec(spec);
751
- if (!status) {
752
- throw new SandboxSetupError(
753
- 'SPEC_STATUS_MISSING',
754
- 'The specification has no status field, so it cannot be approved.',
755
- );
756
- }
757
- if (status[1] !== 'approved') {
758
- spec = spec.replace(/^status:[ \t]*\S+[ \t]*$/m, 'status: approved');
759
- await writeFile(specPath, spec, 'utf8');
760
- }
761
- const approvedAt = Date.now();
762
- const specHash = hashSpec(spec);
763
- const current = await readSession(workspaceRoot, session.id);
764
- return {
765
- session: await updateSession(workspaceRoot, session.id, {
766
- modules: current.modules.map((entry) =>
767
- entry.directory === module.directory
768
- ? { ...entry, specApprovedAt: approvedAt, specHash }
769
- : entry,
770
- ),
771
- state: 'planned',
772
- }),
773
- status: 'approved',
774
- module: module.directory,
775
- };
776
- }
777
-
778
- /* The module a request names, by directory or by id. A name the session does
779
- not have is refused rather than silently redirected to the primary module,
780
- because everything downstream builds a path from it. */
781
- export function findSessionModule(
782
- session: SandboxSession,
783
- requested: string | null | undefined,
784
- ): SessionModule {
785
- if (!requested) return session.modules[0]!;
786
- const found = session.modules.find(
787
- (module) => module.directory === requested || module.id === requested,
788
- );
789
- if (!found) {
790
- throw new SandboxSetupError(
791
- 'MODULE_NOT_IN_SESSION',
792
- `This session has no module ${requested}. Add it to the session first.`,
793
- );
794
- }
795
- return found;
796
- }
797
-
798
- /* The next sequence per session, so an append does not re-read the whole
799
- transcript. Bounded: the map only holds sessions this process appended to. */
800
- const lastSequence = new Map<string, number>();
801
- const SEQUENCE_CACHE_LIMIT = 256;
802
-
803
- export async function appendChatEntry(
804
- workspaceRoot: string,
805
- session: SandboxSession,
806
- entry: Omit<ChatEntry, 'sequence' | 'at'>,
807
- ): Promise<ChatEntry> {
808
- const paths = sessionPaths(workspaceRoot, session.id, session.moduleSuffix);
809
- let previous = lastSequence.get(session.id);
810
- if (previous === undefined) {
811
- previous = (await readChat(workspaceRoot, session)).at(-1)?.sequence ?? 0;
812
- if (lastSequence.size >= SEQUENCE_CACHE_LIMIT) {
813
- lastSequence.delete(lastSequence.keys().next().value!);
814
- }
815
- }
816
- const record: ChatEntry = {
817
- ...entry,
818
- sequence: previous + 1,
819
- at: Date.now(),
820
- };
821
- lastSequence.set(session.id, record.sequence);
822
- await appendFile(paths.chatLog, `${JSON.stringify(record)}\n`, 'utf8');
823
- return record;
824
- }
825
-
826
- export async function readChat(
827
- workspaceRoot: string,
828
- session: SandboxSession,
829
- after = 0,
830
- ): Promise<readonly ChatEntry[]> {
831
- const paths = sessionPaths(workspaceRoot, session.id, session.moduleSuffix);
832
- let raw: string;
833
- try {
834
- raw = await readFile(paths.chatLog, 'utf8');
835
- } catch {
836
- return [];
837
- }
838
- const entries: ChatEntry[] = [];
839
- for (const line of raw.split('\n')) {
840
- if (!line.trim()) continue;
841
- try {
842
- const entry = JSON.parse(line) as ChatEntry;
843
- if (entry.sequence > after) entries.push(entry);
844
- } catch {
845
- continue;
846
- }
847
- }
848
- return entries;
849
- }