@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,230 +0,0 @@
1
- import { readFile, readdir, stat } from 'node:fs/promises';
2
- import { join, relative } from 'node:path';
3
-
4
- export type FileChange = 'created' | 'modified' | 'deleted';
5
-
6
- export interface DiffLine {
7
- readonly type: 'context' | 'add' | 'del';
8
- readonly text: string;
9
- readonly oldLine: number | null;
10
- readonly newLine: number | null;
11
- }
12
-
13
- export interface DiffHunk {
14
- readonly header: string;
15
- readonly lines: readonly DiffLine[];
16
- }
17
-
18
- export interface FileDiff {
19
- readonly path: string;
20
- readonly change: FileChange;
21
- readonly additions: number;
22
- readonly deletions: number;
23
- readonly binary: boolean;
24
- readonly truncated: boolean;
25
- readonly hunks: readonly DiffHunk[];
26
- }
27
-
28
- const IGNORED = new Set(['node_modules', 'dist', '.git']);
29
- const MAX_DIFF_LINES = 4_000;
30
- const CONTEXT_LINES = 3;
31
-
32
- async function listTree(root: string, directory = root): Promise<string[]> {
33
- let entries;
34
- try {
35
- entries = await readdir(directory, { withFileTypes: true });
36
- } catch {
37
- return [];
38
- }
39
- const files: string[] = [];
40
- for (const entry of entries) {
41
- if (IGNORED.has(entry.name)) continue;
42
- const path = join(directory, entry.name);
43
- if (entry.isSymbolicLink()) continue;
44
- if (entry.isDirectory()) files.push(...(await listTree(root, path)));
45
- else files.push(relative(root, path));
46
- }
47
- return files;
48
- }
49
-
50
- async function readIfPresent(path: string): Promise<string | null> {
51
- try {
52
- const info = await stat(path);
53
- if (!info.isFile()) return null;
54
- return await readFile(path, 'utf8');
55
- } catch {
56
- return null;
57
- }
58
- }
59
-
60
- /* Longest common subsequence over lines. Files above the guard are reported as
61
- changed without hunks so one generated lock file cannot stall the sandbox. */
62
- function lineDiff(
63
- before: readonly string[],
64
- after: readonly string[],
65
- ): readonly DiffLine[] {
66
- const rows = before.length;
67
- const columns = after.length;
68
- const table: number[][] = Array.from({ length: rows + 1 }, () =>
69
- new Array<number>(columns + 1).fill(0),
70
- );
71
- for (let row = rows - 1; row >= 0; row -= 1) {
72
- for (let column = columns - 1; column >= 0; column -= 1) {
73
- table[row]![column] =
74
- before[row] === after[column]
75
- ? table[row + 1]![column + 1]! + 1
76
- : Math.max(table[row + 1]![column]!, table[row]![column + 1]!);
77
- }
78
- }
79
- const lines: DiffLine[] = [];
80
- let row = 0;
81
- let column = 0;
82
- while (row < rows && column < columns) {
83
- if (before[row] === after[column]) {
84
- lines.push({
85
- type: 'context',
86
- text: before[row]!,
87
- oldLine: row + 1,
88
- newLine: column + 1,
89
- });
90
- row += 1;
91
- column += 1;
92
- } else if (table[row + 1]![column]! >= table[row]![column + 1]!) {
93
- lines.push({
94
- type: 'del',
95
- text: before[row]!,
96
- oldLine: row + 1,
97
- newLine: null,
98
- });
99
- row += 1;
100
- } else {
101
- lines.push({
102
- type: 'add',
103
- text: after[column]!,
104
- oldLine: null,
105
- newLine: column + 1,
106
- });
107
- column += 1;
108
- }
109
- }
110
- while (row < rows) {
111
- lines.push({
112
- type: 'del',
113
- text: before[row]!,
114
- oldLine: row + 1,
115
- newLine: null,
116
- });
117
- row += 1;
118
- }
119
- while (column < columns) {
120
- lines.push({
121
- type: 'add',
122
- text: after[column]!,
123
- oldLine: null,
124
- newLine: column + 1,
125
- });
126
- column += 1;
127
- }
128
- return lines;
129
- }
130
-
131
- function groupHunks(lines: readonly DiffLine[]): readonly DiffHunk[] {
132
- const changedIndexes = lines
133
- .map((line, index) => (line.type === 'context' ? -1 : index))
134
- .filter((index) => index >= 0);
135
- if (changedIndexes.length === 0) return [];
136
-
137
- const hunks: DiffHunk[] = [];
138
- let start = Math.max(0, changedIndexes[0]! - CONTEXT_LINES);
139
- let end = Math.min(lines.length - 1, changedIndexes[0]! + CONTEXT_LINES);
140
- for (const index of changedIndexes.slice(1)) {
141
- if (index - end <= CONTEXT_LINES * 2) {
142
- end = Math.min(lines.length - 1, index + CONTEXT_LINES);
143
- continue;
144
- }
145
- hunks.push(buildHunk(lines, start, end));
146
- start = Math.max(0, index - CONTEXT_LINES);
147
- end = Math.min(lines.length - 1, index + CONTEXT_LINES);
148
- }
149
- hunks.push(buildHunk(lines, start, end));
150
- return hunks;
151
- }
152
-
153
- function buildHunk(
154
- lines: readonly DiffLine[],
155
- start: number,
156
- end: number,
157
- ): DiffHunk {
158
- const slice = lines.slice(start, end + 1);
159
- const firstOld = slice.find((line) => line.oldLine !== null)?.oldLine ?? 0;
160
- const firstNew = slice.find((line) => line.newLine !== null)?.newLine ?? 0;
161
- const oldCount = slice.filter((line) => line.type !== 'add').length;
162
- const newCount = slice.filter((line) => line.type !== 'del').length;
163
- return {
164
- header: `@@ -${firstOld},${oldCount} +${firstNew},${newCount} @@`,
165
- lines: slice,
166
- };
167
- }
168
-
169
- export async function diffFile(
170
- beforePath: string,
171
- afterPath: string,
172
- path: string,
173
- ): Promise<FileDiff | null> {
174
- const before = await readIfPresent(beforePath);
175
- const after = await readIfPresent(afterPath);
176
- if (before === null && after === null) return null;
177
- if (before === after) return null;
178
-
179
- const change: FileChange =
180
- before === null ? 'created' : after === null ? 'deleted' : 'modified';
181
- const beforeLines = before === null ? [] : before.split('\n');
182
- const afterLines = after === null ? [] : after.split('\n');
183
- const binary = [before, after].some(
184
- (content) => content !== null && content.includes(String.fromCharCode(0)),
185
- );
186
- const truncated =
187
- beforeLines.length + afterLines.length > MAX_DIFF_LINES || binary;
188
-
189
- if (truncated) {
190
- return {
191
- path,
192
- change,
193
- additions: afterLines.length,
194
- deletions: beforeLines.length,
195
- binary,
196
- truncated: true,
197
- hunks: [],
198
- };
199
- }
200
- const lines = lineDiff(beforeLines, afterLines);
201
- return {
202
- path,
203
- change,
204
- additions: lines.filter((line) => line.type === 'add').length,
205
- deletions: lines.filter((line) => line.type === 'del').length,
206
- binary: false,
207
- truncated: false,
208
- hunks: groupHunks(lines),
209
- };
210
- }
211
-
212
- export async function diffTrees(
213
- basePath: string,
214
- currentPath: string,
215
- ): Promise<readonly FileDiff[]> {
216
- const paths = new Set([
217
- ...(await listTree(basePath)),
218
- ...(await listTree(currentPath)),
219
- ]);
220
- const diffs: FileDiff[] = [];
221
- for (const path of [...paths].sort()) {
222
- const diff = await diffFile(
223
- join(basePath, path),
224
- join(currentPath, path),
225
- path,
226
- );
227
- if (diff) diffs.push(diff);
228
- }
229
- return diffs;
230
- }
@@ -1,388 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { access } from 'node:fs/promises';
3
- import { join } from 'node:path';
4
- import { inspectAutoReview } from './auto-review.ts';
5
- import { checkDeclaredDependencies } from './dependencies.ts';
6
- import {
7
- modulePathOf,
8
- type SandboxSession,
9
- type SessionModule,
10
- type SessionPaths,
11
- } from './sessions.ts';
12
-
13
- export type GateId =
14
- | 'spec-schema'
15
- | 'module-schema'
16
- | 'dependencies'
17
- | 'typecheck'
18
- | 'tests'
19
- | 'format'
20
- | 'auto-review';
21
-
22
- export interface GateResult {
23
- readonly id: GateId;
24
- /* The draft module directory a module-level gate ran in; absent for a gate
25
- that checks the whole session workspace. */
26
- readonly module?: string;
27
- readonly status: 'passed' | 'failed' | 'skipped';
28
- readonly durationMs: number;
29
- readonly command: string;
30
- readonly output: string;
31
- }
32
-
33
- interface GateDefinition {
34
- readonly id: GateId;
35
- readonly summary: string;
36
- /* Workspace gates run once per session; module gates run per draft module. */
37
- readonly scope: 'workspace' | 'module';
38
- command(context: GateContext): {
39
- readonly command: string;
40
- readonly args: readonly string[];
41
- readonly cwd: string;
42
- } | null;
43
- /* A gate the sandbox answers itself, without spawning a process. */
44
- inspect?(context: GateContext): Promise<{
45
- readonly passed: boolean;
46
- readonly output: string;
47
- }>;
48
- }
49
-
50
- interface GateContext {
51
- readonly workspaceRoot: string;
52
- readonly paths: SessionPaths;
53
- readonly session: SandboxSession;
54
- readonly module: SessionModule;
55
- readonly modulePath: string;
56
- readonly signal?: AbortSignal | undefined;
57
- }
58
-
59
- /* Output keeps its head and its tail: the head names what failed (the test
60
- list, the first compiler errors), the tail carries the summary. */
61
- const HEAD_LIMIT = 10_000;
62
- const TAIL_LIMIT = 6_000;
63
- const GATE_TIMEOUT_MS = 5 * 60 * 1000;
64
-
65
- /* The only commands the sandbox may run. Agents never execute anything: they
66
- request a gate by id and the orchestrator runs this fixed list. */
67
- const GATE_DEFINITIONS: readonly GateDefinition[] = [
68
- {
69
- id: 'auto-review',
70
- summary: 'Review evidence matches the current module contents.',
71
- scope: 'module',
72
- command: () => null,
73
- inspect: (context) => inspectAutoReview(context.paths, context.module),
74
- },
75
- {
76
- id: 'spec-schema',
77
- summary: 'Validate the module specification against its schema.',
78
- scope: 'workspace',
79
- command: (context) => ({
80
- command: 'pnpm',
81
- args: [
82
- '--dir',
83
- context.workspaceRoot,
84
- '--silent',
85
- 'flowdular',
86
- 'spec',
87
- 'validate',
88
- '--all',
89
- '--json',
90
- '--root',
91
- context.paths.workspace,
92
- ],
93
- cwd: context.workspaceRoot,
94
- }),
95
- },
96
- {
97
- id: 'module-schema',
98
- summary: 'Validate the module manifest and registry references.',
99
- scope: 'workspace',
100
- command: (context) => ({
101
- command: 'pnpm',
102
- args: [
103
- '--dir',
104
- context.workspaceRoot,
105
- '--silent',
106
- 'flowdular',
107
- 'module',
108
- 'validate',
109
- '--json',
110
- '--root',
111
- context.paths.workspace,
112
- /* The other modules in the session are manifest-only copies for the
113
- registry graph; file-level checks run against the drafts only. */
114
- '--module',
115
- context.session.modules.map((entry) => entry.id).join(','),
116
- ],
117
- cwd: context.workspaceRoot,
118
- }),
119
- },
120
- {
121
- id: 'dependencies',
122
- summary: 'Every imported package is declared by the module manifest.',
123
- scope: 'module',
124
- command: () => null,
125
- inspect: async (context) => {
126
- const report = await checkDeclaredDependencies(context.modulePath);
127
- return {
128
- passed: report.missing.length === 0,
129
- output:
130
- report.missing.length === 0
131
- ? `${report.imported.length} imported packages, all declared.`
132
- : `Undeclared packages: ${report.missing.join(', ')}. Add them to package.json dependencies; the session installs what package.json declares and nothing else, and the ejected module would fail without them.`,
133
- };
134
- },
135
- },
136
- {
137
- id: 'typecheck',
138
- summary: 'Typecheck the module with the workspace compiler.',
139
- scope: 'module',
140
- command: (context) => ({
141
- command: join(context.modulePath, 'node_modules', '.bin', 'tsrx-tsc'),
142
- args: ['--noEmit', '-p', 'tsconfig.json'],
143
- cwd: context.modulePath,
144
- }),
145
- },
146
- {
147
- id: 'tests',
148
- summary: 'Run the module test suite.',
149
- scope: 'module',
150
- command: (context) => ({
151
- command: join(context.modulePath, 'node_modules', '.bin', 'vitest'),
152
- args: ['run', '--passWithNoTests=false'],
153
- cwd: context.modulePath,
154
- }),
155
- },
156
- {
157
- id: 'format',
158
- summary: 'Check formatting with the workspace Prettier configuration.',
159
- scope: 'module',
160
- command: (context) => ({
161
- command: join(context.workspaceRoot, 'node_modules', '.bin', 'prettier'),
162
- args: ['--check', '.'],
163
- cwd: context.modulePath,
164
- }),
165
- },
166
- ];
167
-
168
- export const GATE_IDS = GATE_DEFINITIONS.map((gate) => gate.id);
169
-
170
- /* Formatting is mechanical, so the sandbox fixes it on request instead of
171
- spending an agent turn on it. It is still the orchestrator running the
172
- command, never the model. */
173
- export async function formatDirectory(
174
- workspaceRoot: string,
175
- directory: string,
176
- ): Promise<GateResult> {
177
- const command = join(workspaceRoot, 'node_modules', '.bin', 'prettier');
178
- const startedAt = Date.now();
179
- try {
180
- await access(command);
181
- } catch {
182
- return {
183
- id: 'format',
184
- status: 'skipped',
185
- durationMs: 0,
186
- command,
187
- output: 'Prettier is not installed in this workspace.',
188
- };
189
- }
190
- /* An operator asked for this directly, so it is not tied to a turn and has
191
- no turn signal to observe. */
192
- const result = await runProcess(command, ['--write', '.'], directory);
193
- return {
194
- id: 'format',
195
- status: result.code === 0 ? 'passed' : 'failed',
196
- durationMs: Date.now() - startedAt,
197
- command: `${command} --write .`,
198
- output: result.output.trim(),
199
- };
200
- }
201
-
202
- export async function formatSession(context: {
203
- readonly workspaceRoot: string;
204
- readonly paths: SessionPaths;
205
- readonly session: SandboxSession;
206
- }): Promise<readonly GateResult[]> {
207
- const results: GateResult[] = [];
208
- for (const module of context.session.modules) {
209
- results.push({
210
- ...(await formatDirectory(
211
- context.workspaceRoot,
212
- modulePathOf(context.paths, module.directory),
213
- )),
214
- module: module.directory,
215
- });
216
- }
217
- return results;
218
- }
219
-
220
- export function isGateId(value: string): value is GateId {
221
- return (GATE_IDS as readonly string[]).includes(value);
222
- }
223
-
224
- function runProcess(
225
- command: string,
226
- args: readonly string[],
227
- cwd: string,
228
- signal?: AbortSignal | undefined,
229
- ): Promise<{ code: number | null; output: string }> {
230
- return new Promise((resolvePromise) => {
231
- if (signal?.aborted) {
232
- resolvePromise({ code: null, output: 'The gate was stopped.' });
233
- return;
234
- }
235
- const child = spawn(command, [...args], {
236
- cwd,
237
- env: { ...process.env, CI: 'true', FORCE_COLOR: '0' },
238
- stdio: ['ignore', 'pipe', 'pipe'],
239
- });
240
- let head = '';
241
- let tail = '';
242
- let omitted = 0;
243
- const append = (chunk: string) => {
244
- if (head.length < HEAD_LIMIT) {
245
- const room = HEAD_LIMIT - head.length;
246
- head += chunk.slice(0, room);
247
- chunk = chunk.slice(room);
248
- if (!chunk) return;
249
- }
250
- const kept = (tail + chunk).slice(-TAIL_LIMIT);
251
- omitted += tail.length + chunk.length - kept.length;
252
- tail = kept;
253
- };
254
- const output = () =>
255
- omitted > 0
256
- ? `${head}\n[... ${omitted} characters omitted ...]\n${tail}`
257
- : head + tail;
258
- child.stdout.setEncoding('utf8');
259
- child.stderr.setEncoding('utf8');
260
- child.stdout.on('data', append);
261
- child.stderr.on('data', append);
262
- const timer = setTimeout(() => {
263
- append('\nThe gate exceeded its time budget and was stopped.');
264
- child.kill('SIGKILL');
265
- }, GATE_TIMEOUT_MS);
266
- timer.unref();
267
- const stop = () => {
268
- append('\nThe turn was stopped, so the gate was stopped with it.');
269
- child.kill('SIGKILL');
270
- };
271
- signal?.addEventListener('abort', stop, { once: true });
272
- const settle = (value: { code: number | null; output: string }) => {
273
- clearTimeout(timer);
274
- signal?.removeEventListener('abort', stop);
275
- resolvePromise(value);
276
- };
277
- child.on('error', (error) => {
278
- settle({ code: null, output: `${output()}\n${error.message}` });
279
- });
280
- child.on('close', (code) => {
281
- settle({ code, output: output() });
282
- });
283
- });
284
- }
285
-
286
- async function runGate(
287
- definition: GateDefinition,
288
- context: GateContext,
289
- ): Promise<GateResult> {
290
- const id = definition.id;
291
- const module =
292
- definition.scope === 'module' ? { module: context.module.directory } : {};
293
- const startedAt = Date.now();
294
- if (definition.inspect) {
295
- const result = await definition.inspect(context);
296
- return {
297
- id,
298
- ...module,
299
- status: result.passed ? 'passed' : 'failed',
300
- durationMs: Date.now() - startedAt,
301
- command: definition.summary,
302
- output: result.output,
303
- };
304
- }
305
- const invocation = definition.command(context);
306
- if (!invocation) {
307
- return {
308
- id,
309
- ...module,
310
- status: 'skipped',
311
- durationMs: 0,
312
- command: '',
313
- output: 'This gate does not apply to the session.',
314
- };
315
- }
316
- const printable = `${invocation.command} ${invocation.args.join(' ')}`;
317
- if (invocation.command.includes('/')) {
318
- try {
319
- await access(invocation.command);
320
- } catch {
321
- return {
322
- id,
323
- ...module,
324
- status: 'skipped',
325
- durationMs: 0,
326
- command: printable,
327
- output: `${invocation.command} is not installed in this session workspace. The module's package.json must declare it as a devDependency.`,
328
- };
329
- }
330
- }
331
- const result = await runProcess(
332
- invocation.command,
333
- invocation.args,
334
- invocation.cwd,
335
- context.signal,
336
- );
337
- return {
338
- id,
339
- ...module,
340
- status: result.code === 0 ? 'passed' : 'failed',
341
- durationMs: Date.now() - startedAt,
342
- command: printable,
343
- output: result.output.trim(),
344
- };
345
- }
346
-
347
- export interface RunGatesInput {
348
- readonly workspaceRoot: string;
349
- readonly paths: SessionPaths;
350
- readonly session: SandboxSession;
351
- readonly gates: readonly GateId[];
352
- /* The draft modules to gate; every module of the session by default. A turn
353
- passes the ones that changed, so an untouched module is not re-checked. */
354
- readonly modules?: readonly SessionModule[];
355
- /* Stopping a turn must stop its gates. Without this a spawned gate outlives
356
- the abort and the session stays busy until the gate's own budget expires. */
357
- readonly signal?: AbortSignal | undefined;
358
- }
359
-
360
- /* Workspace gates run once; module gates run once per draft module, so a
361
- session that touches several modules reports each of them. */
362
- export async function runGates(
363
- input: RunGatesInput,
364
- ): Promise<readonly GateResult[]> {
365
- const results: GateResult[] = [];
366
- const modules = input.modules ?? input.session.modules;
367
- for (const id of input.gates) {
368
- const definition = GATE_DEFINITIONS.find((gate) => gate.id === id)!;
369
- const targets =
370
- definition.scope === 'workspace' ? modules.slice(0, 1) : modules;
371
- for (const module of targets) {
372
- /* A stopped turn runs no further gates. The one already spawned is
373
- killed through the same signal. */
374
- if (input.signal?.aborted) return results;
375
- results.push(
376
- await runGate(definition, {
377
- workspaceRoot: input.workspaceRoot,
378
- paths: input.paths,
379
- session: input.session,
380
- module,
381
- modulePath: modulePathOf(input.paths, module.directory),
382
- signal: input.signal,
383
- }),
384
- );
385
- }
386
- }
387
- return results;
388
- }