@flowdular/sdk 0.1.0 → 0.2.1
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.
- package/.ai/skills/test-hardening/SKILL.md +1 -1
- package/README.md +69 -12
- package/assets/flowdular-banner.png +0 -0
- package/modules/agents/package.json +1 -1
- package/modules/auth/package.json +1 -1
- package/modules/auth/src/client/AuthenticationCore.tsrx +3 -1
- package/modules/auth/src/server/endpoints.ts +3 -1
- package/modules/auth/src/server/runtime.ts +4 -0
- package/modules/automations/package.json +1 -1
- package/modules/automations-workflows-integration/package.json +1 -1
- package/modules/profile/package.json +1 -1
- package/modules/sandbox/package.json +1 -1
- package/modules/system/package.json +1 -1
- package/modules/users/package.json +1 -1
- package/modules/workflows/package.json +1 -1
- package/package.json +5 -13
- package/packages/ai-provider/package.json +1 -1
- package/packages/cli-protocol/package.json +1 -1
- package/packages/client/package.json +1 -1
- package/packages/client/src/routing.ts +39 -2
- package/packages/client/src/shell/BrandMark.tsrx +5 -1
- package/packages/client/src/shell/navigation.ts +3 -1
- package/packages/client/src/state.ts +6 -2
- package/packages/contracts/package.json +1 -1
- package/packages/contracts/schemas/project.schema.json +16 -42
- package/packages/database/package.json +1 -1
- package/packages/database-pglite/package.json +2 -2
- package/packages/database-pglite/src/driver.ts +21 -3
- package/packages/database-testing/package.json +1 -1
- package/packages/dev-console/package.json +1 -1
- package/packages/harness/package.json +1 -1
- package/packages/kernel/package.json +1 -1
- package/packages/server/package.json +1 -1
- package/packages/server/src/application-routes.ts +80 -8
- package/packages/server/src/index.ts +4 -1
- package/packages/server/src/web-html.ts +9 -5
- package/packages/server/src/web.ts +59 -7
- package/packages/ui/package.json +1 -1
- package/packages/coding-agent/package.json +0 -44
- package/packages/coding-agent/src/drivers/byok.ts +0 -322
- package/packages/coding-agent/src/drivers/claude-code.ts +0 -290
- package/packages/coding-agent/src/drivers/codex.ts +0 -281
- package/packages/coding-agent/src/index.ts +0 -56
- package/packages/coding-agent/src/registry.ts +0 -119
- package/packages/coding-agent/src/roles/contract.ts +0 -60
- package/packages/coding-agent/src/roles/defaults.ts +0 -134
- package/packages/coding-agent/src/roles/handoff.ts +0 -41
- package/packages/coding-agent/src/roles/registry.ts +0 -156
- package/packages/coding-agent/src/roles/skills.ts +0 -72
- package/packages/coding-agent/src/roles/sync.ts +0 -88
- package/packages/coding-agent/src/types.ts +0 -102
- package/packages/coding-agent/src/workspace.ts +0 -275
- package/packages/sandbox/README.md +0 -691
- package/packages/sandbox/bin/flowdular-sandbox.mjs +0 -393
- package/packages/sandbox/dist/client/assets/App-ClMOrpyg.js +0 -11
- package/packages/sandbox/dist/client/assets/PreviewHost-BHiHrfbQ.js +0 -1
- package/packages/sandbox/dist/client/assets/auto-buGEq6de.js +0 -3
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
- package/packages/sandbox/dist/client/assets/index-CvwhlQ87.js +0 -4
- package/packages/sandbox/dist/client/assets/styles-BPWaGf55.js +0 -3
- package/packages/sandbox/dist/client/assets/styles-Dx1_KOag.css +0 -1
- package/packages/sandbox/dist/client/favicon.svg +0 -1
- package/packages/sandbox/dist/server/assets/auto-CVGNYyb3.js +0 -12785
- package/packages/sandbox/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +0 -4125
- package/packages/sandbox/dist/server/assets/nodefs-DVSI7kG0.js +0 -24
- package/packages/sandbox/dist/server/assets/opfs-ahp-D56iokt1.js +0 -388
- package/packages/sandbox/dist/server/assets/token-Bvaq7Anz.js +0 -55
- package/packages/sandbox/dist/server/assets/token-util-BqSvdIPQ.js +0 -3
- package/packages/sandbox/dist/server/assets/token-util-CktFFT4z.js +0 -494
- package/packages/sandbox/dist/server/entry.js +0 -91671
- package/packages/sandbox/dist/server/favicon.svg +0 -1
- package/packages/sandbox/dist/server/index.html +0 -161
- package/packages/sandbox/index.html +0 -159
- package/packages/sandbox/octane.config.ts +0 -36
- package/packages/sandbox/package.json +0 -83
- package/packages/sandbox/public/favicon.svg +0 -1
- package/packages/sandbox/public/og.png +0 -0
- package/packages/sandbox/src/App.tsrx +0 -924
- package/packages/sandbox/src/client/ApprovalHandoff.tsrx +0 -39
- package/packages/sandbox/src/client/ChatPane.tsrx +0 -694
- package/packages/sandbox/src/client/ConnectView.tsrx +0 -86
- package/packages/sandbox/src/client/DiffPane.tsrx +0 -150
- package/packages/sandbox/src/client/EjectModal.tsrx +0 -571
- package/packages/sandbox/src/client/GateResults.tsrx +0 -60
- package/packages/sandbox/src/client/GitHubSettingsModal.tsrx +0 -219
- package/packages/sandbox/src/client/NewSessionForm.tsrx +0 -214
- package/packages/sandbox/src/client/SessionBar.tsrx +0 -151
- package/packages/sandbox/src/client/SessionPicker.tsrx +0 -472
- package/packages/sandbox/src/client/SpecEditor.tsrx +0 -109
- package/packages/sandbox/src/client/SpecReviewCard.tsrx +0 -321
- package/packages/sandbox/src/client/WorkCard.tsrx +0 -128
- package/packages/sandbox/src/client/WorkbenchModal.tsrx +0 -161
- package/packages/sandbox/src/client/WorkspaceMenu.tsrx +0 -100
- package/packages/sandbox/src/client/api.ts +0 -797
- package/packages/sandbox/src/client/attachments.ts +0 -73
- package/packages/sandbox/src/client/dashboard.ts +0 -46
- package/packages/sandbox/src/client/github-settings.ts +0 -48
- package/packages/sandbox/src/client/highlight.ts +0 -105
- package/packages/sandbox/src/client/i18n.ts +0 -32
- package/packages/sandbox/src/client/locales/en.json +0 -466
- package/packages/sandbox/src/client/locales/pl.json +0 -466
- package/packages/sandbox/src/client/session-labels.ts +0 -77
- package/packages/sandbox/src/client/state.ts +0 -108
- package/packages/sandbox/src/index.ts +0 -10
- package/packages/sandbox/src/preview/PreviewHost.tsrx +0 -373
- package/packages/sandbox/src/preview/load-module.ts +0 -16
- package/packages/sandbox/src/server/attachments.ts +0 -308
- package/packages/sandbox/src/server/auto-review.ts +0 -181
- package/packages/sandbox/src/server/checkpoints.ts +0 -90
- package/packages/sandbox/src/server/config.ts +0 -460
- package/packages/sandbox/src/server/dashboard.ts +0 -245
- package/packages/sandbox/src/server/delivery/configuration.ts +0 -193
- package/packages/sandbox/src/server/delivery/git-pr.ts +0 -1473
- package/packages/sandbox/src/server/delivery/index.ts +0 -83
- package/packages/sandbox/src/server/delivery/local.ts +0 -151
- package/packages/sandbox/src/server/delivery/official-modules.ts +0 -516
- package/packages/sandbox/src/server/delivery/plan.ts +0 -218
- package/packages/sandbox/src/server/delivery/policies.ts +0 -239
- package/packages/sandbox/src/server/delivery/record.ts +0 -41
- package/packages/sandbox/src/server/delivery/steps.ts +0 -464
- package/packages/sandbox/src/server/delivery/types.ts +0 -158
- package/packages/sandbox/src/server/dependencies.ts +0 -88
- package/packages/sandbox/src/server/diff.ts +0 -230
- package/packages/sandbox/src/server/gates.ts +0 -388
- package/packages/sandbox/src/server/index.ts +0 -139
- package/packages/sandbox/src/server/path-guard.ts +0 -282
- package/packages/sandbox/src/server/planning.ts +0 -749
- package/packages/sandbox/src/server/platform-client.ts +0 -189
- package/packages/sandbox/src/server/preview-database-host.ts +0 -340
- package/packages/sandbox/src/server/preview-database-protocol.ts +0 -163
- package/packages/sandbox/src/server/preview-database-proxy.ts +0 -356
- package/packages/sandbox/src/server/preview-database.ts +0 -109
- package/packages/sandbox/src/server/preview-ipc.ts +0 -15
- package/packages/sandbox/src/server/preview-modules.ts +0 -83
- package/packages/sandbox/src/server/preview-revision.ts +0 -44
- package/packages/sandbox/src/server/preview-runtime.ts +0 -467
- package/packages/sandbox/src/server/preview-worker-manager.ts +0 -436
- package/packages/sandbox/src/server/preview-worker.ts +0 -168
- package/packages/sandbox/src/server/reference.ts +0 -251
- package/packages/sandbox/src/server/routes.ts +0 -2258
- package/packages/sandbox/src/server/runtime.ts +0 -188
- package/packages/sandbox/src/server/session-owner.ts +0 -23
- package/packages/sandbox/src/server/sessions.ts +0 -849
- package/packages/sandbox/src/server/spec.ts +0 -471
- package/packages/sandbox/src/server/turn-lifecycle.ts +0 -56
- package/packages/sandbox/src/server/turns.ts +0 -993
- package/packages/sandbox/src/server/workspace-install.ts +0 -266
- package/packages/sandbox/src/server/workspace-root.ts +0 -73
- package/packages/sandbox/src/styles.css +0 -1573
- package/packages/sandbox/vite.config.ts +0 -133
|
@@ -1,460 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
flowdularEnvironment,
|
|
3
|
-
flowdularStateDirectory,
|
|
4
|
-
} from '@flowdular/sdk/kernel/runtime-config';
|
|
5
|
-
import {
|
|
6
|
-
createCipheriv,
|
|
7
|
-
createDecipheriv,
|
|
8
|
-
createHash,
|
|
9
|
-
randomBytes,
|
|
10
|
-
} from 'node:crypto';
|
|
11
|
-
import { constants } from 'node:fs';
|
|
12
|
-
import { chmod, lstat, mkdir, open } from 'node:fs/promises';
|
|
13
|
-
import { join } from 'node:path';
|
|
14
|
-
import type { SandboxRuntimeMode } from '@flowdular/sdk/coding-agent';
|
|
15
|
-
import type { AiProviderKind } from '@flowdular/sdk/ai-provider';
|
|
16
|
-
import { SandboxSetupError } from './workspace-root.ts';
|
|
17
|
-
|
|
18
|
-
export const SANDBOX_DIRECTORY = '.flowdular/sandbox';
|
|
19
|
-
|
|
20
|
-
export function sandboxDirectory(workspaceRoot: string): string {
|
|
21
|
-
// The parent selects this path before applying the worker filesystem ceiling.
|
|
22
|
-
// A worker cannot inspect sibling state roots to rediscover that selection.
|
|
23
|
-
if (process.env.FD_INTERNAL_SANDBOX_PREVIEW_WORKER === '1') {
|
|
24
|
-
const root = process.env.FD_INTERNAL_SANDBOX_STATE_ROOT;
|
|
25
|
-
if (root !== '.flowdular' && root !== '.coreloom')
|
|
26
|
-
throw new Error('Missing preview state root.');
|
|
27
|
-
return join(workspaceRoot, root, 'sandbox');
|
|
28
|
-
}
|
|
29
|
-
return join(flowdularStateDirectory(workspaceRoot), 'sandbox');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export type PreviewDataMode = 'fixtures' | 'bridge';
|
|
33
|
-
|
|
34
|
-
export interface SealedSecret {
|
|
35
|
-
readonly iv: string;
|
|
36
|
-
readonly tag: string;
|
|
37
|
-
readonly ciphertext: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface ByokProviderConfiguration {
|
|
41
|
-
readonly kind: AiProviderKind;
|
|
42
|
-
readonly model: string;
|
|
43
|
-
readonly resourceName?: string;
|
|
44
|
-
readonly baseURL?: string;
|
|
45
|
-
readonly credential: SealedSecret | null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export type GitHubDeliveryMode = 'auto' | 'direct' | 'fork';
|
|
49
|
-
|
|
50
|
-
/* Repository delivery is local sandbox configuration, not project source.
|
|
51
|
-
One operator can use a fork while another can push directly without either
|
|
52
|
-
changing flowdular.json for the whole team. */
|
|
53
|
-
export interface GitHubDeliveryConfiguration {
|
|
54
|
-
readonly enabled: boolean;
|
|
55
|
-
/* False keeps the repository-owned sandbox.delivery.git values authoritative.
|
|
56
|
-
True applies the operator's machine-local remote and branch preferences. */
|
|
57
|
-
readonly overridesProject: boolean;
|
|
58
|
-
readonly remote: string;
|
|
59
|
-
/* owner/name of the repository that receives the pull request. Null means
|
|
60
|
-
derive it from the configured remote. */
|
|
61
|
-
readonly repository: string | null;
|
|
62
|
-
readonly baseBranch: string;
|
|
63
|
-
readonly branchPrefix: string;
|
|
64
|
-
readonly mode: GitHubDeliveryMode;
|
|
65
|
-
readonly forkOwner: string | null;
|
|
66
|
-
readonly reviewers: readonly string[];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export interface SandboxConfiguration {
|
|
70
|
-
readonly version: 1;
|
|
71
|
-
readonly mode: SandboxRuntimeMode;
|
|
72
|
-
/* Origin of the Flowdular application this sandbox connects to. It may be a
|
|
73
|
-
local development server or a remote deployment. */
|
|
74
|
-
readonly platformUrl: string;
|
|
75
|
-
readonly platformToken: SealedSecret | null;
|
|
76
|
-
readonly driver: string;
|
|
77
|
-
readonly driverModel: string | null;
|
|
78
|
-
readonly previewData: PreviewDataMode;
|
|
79
|
-
readonly byok: ByokProviderConfiguration | null;
|
|
80
|
-
/* Token for the pull-request provider of the git-pr delivery, handed to gh
|
|
81
|
-
as GH_TOKEN when the operator's own gh login is not usable. */
|
|
82
|
-
readonly gitProviderToken: SealedSecret | null;
|
|
83
|
-
readonly github: GitHubDeliveryConfiguration;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export const DEFAULT_GITHUB_CONFIGURATION: GitHubDeliveryConfiguration = {
|
|
87
|
-
enabled: true,
|
|
88
|
-
overridesProject: false,
|
|
89
|
-
remote: 'origin',
|
|
90
|
-
repository: null,
|
|
91
|
-
baseBranch: 'main',
|
|
92
|
-
branchPrefix: 'sandbox',
|
|
93
|
-
mode: 'auto',
|
|
94
|
-
forkOwner: null,
|
|
95
|
-
reviewers: [],
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
export const DEFAULT_CONFIGURATION: SandboxConfiguration = {
|
|
99
|
-
version: 1,
|
|
100
|
-
mode: 'loopback',
|
|
101
|
-
platformUrl: 'http://127.0.0.1:4310',
|
|
102
|
-
platformToken: null,
|
|
103
|
-
driver: 'claude-code',
|
|
104
|
-
driverModel: null,
|
|
105
|
-
previewData: 'fixtures',
|
|
106
|
-
byok: null,
|
|
107
|
-
gitProviderToken: null,
|
|
108
|
-
github: DEFAULT_GITHUB_CONFIGURATION,
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const GIT_NAME = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
112
|
-
const MAX_GIT_NAME_LENGTH = 120;
|
|
113
|
-
const MAX_GITHUB_REVIEWERS = 20;
|
|
114
|
-
const GITHUB_REPOSITORY =
|
|
115
|
-
/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]{1,100}$/;
|
|
116
|
-
const GITHUB_ACCOUNT = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
|
|
117
|
-
|
|
118
|
-
function invalidGitHub(detail: string): SandboxSetupError {
|
|
119
|
-
return new SandboxSetupError(
|
|
120
|
-
'GITHUB_CONFIG_INVALID',
|
|
121
|
-
`The GitHub integration is not usable: ${detail}`,
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function assertGitName(value: string, field: string): string {
|
|
126
|
-
if (value.length > MAX_GIT_NAME_LENGTH || !GIT_NAME.test(value)) {
|
|
127
|
-
throw invalidGitHub(`${field} must be a plain git name.`);
|
|
128
|
-
}
|
|
129
|
-
return value;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function assertGitHubRepository(value: string | null): string | null {
|
|
133
|
-
if (value === null || value === '') return null;
|
|
134
|
-
if (!GITHUB_REPOSITORY.test(value)) {
|
|
135
|
-
throw invalidGitHub('repository must use the owner/name form.');
|
|
136
|
-
}
|
|
137
|
-
return value;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export function assertGitHubAccount(value: string | null): string | null {
|
|
141
|
-
if (value === null || value === '') return null;
|
|
142
|
-
if (!GITHUB_ACCOUNT.test(value)) {
|
|
143
|
-
throw invalidGitHub('forkOwner must be a GitHub account name.');
|
|
144
|
-
}
|
|
145
|
-
return value;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export function assertGitHubDeliveryMode(value: string): GitHubDeliveryMode {
|
|
149
|
-
if (value !== 'auto' && value !== 'direct' && value !== 'fork') {
|
|
150
|
-
throw invalidGitHub('mode must be auto, direct, or fork.');
|
|
151
|
-
}
|
|
152
|
-
return value;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function resolveGitHubConfiguration(
|
|
156
|
-
value: Partial<GitHubDeliveryConfiguration> | undefined,
|
|
157
|
-
): GitHubDeliveryConfiguration {
|
|
158
|
-
const defaults = DEFAULT_GITHUB_CONFIGURATION;
|
|
159
|
-
if (
|
|
160
|
-
value?.overridesProject !== undefined &&
|
|
161
|
-
typeof value.overridesProject !== 'boolean'
|
|
162
|
-
) {
|
|
163
|
-
throw invalidGitHub('overridesProject must be a boolean.');
|
|
164
|
-
}
|
|
165
|
-
const reviewers = value?.reviewers ?? defaults.reviewers;
|
|
166
|
-
if (
|
|
167
|
-
!Array.isArray(reviewers) ||
|
|
168
|
-
reviewers.length > MAX_GITHUB_REVIEWERS ||
|
|
169
|
-
!reviewers.every((reviewer) => GITHUB_ACCOUNT.test(reviewer))
|
|
170
|
-
) {
|
|
171
|
-
throw invalidGitHub('reviewers must list GitHub account names.');
|
|
172
|
-
}
|
|
173
|
-
const resolved = {
|
|
174
|
-
enabled: value?.enabled ?? defaults.enabled,
|
|
175
|
-
remote: assertGitName(value?.remote ?? defaults.remote, 'remote'),
|
|
176
|
-
repository: assertGitHubRepository(value?.repository ?? null),
|
|
177
|
-
baseBranch: assertGitName(
|
|
178
|
-
value?.baseBranch ?? defaults.baseBranch,
|
|
179
|
-
'baseBranch',
|
|
180
|
-
),
|
|
181
|
-
branchPrefix: assertGitName(
|
|
182
|
-
value?.branchPrefix ?? defaults.branchPrefix,
|
|
183
|
-
'branchPrefix',
|
|
184
|
-
),
|
|
185
|
-
mode: assertGitHubDeliveryMode(value?.mode ?? defaults.mode),
|
|
186
|
-
forkOwner: assertGitHubAccount(value?.forkOwner ?? null),
|
|
187
|
-
reviewers: [...new Set(reviewers)],
|
|
188
|
-
};
|
|
189
|
-
/* Older config files have no explicit override marker. Preserve genuine
|
|
190
|
-
custom values, but do not let a fully materialized default object shadow
|
|
191
|
-
the repository-owned delivery configuration. */
|
|
192
|
-
const differsFromDefault =
|
|
193
|
-
resolved.enabled !== defaults.enabled ||
|
|
194
|
-
resolved.remote !== defaults.remote ||
|
|
195
|
-
resolved.repository !== defaults.repository ||
|
|
196
|
-
resolved.baseBranch !== defaults.baseBranch ||
|
|
197
|
-
resolved.branchPrefix !== defaults.branchPrefix ||
|
|
198
|
-
resolved.mode !== defaults.mode ||
|
|
199
|
-
resolved.forkOwner !== defaults.forkOwner ||
|
|
200
|
-
resolved.reviewers.length !== defaults.reviewers.length ||
|
|
201
|
-
resolved.reviewers.some(
|
|
202
|
-
(reviewer, index) => reviewer !== defaults.reviewers[index],
|
|
203
|
-
);
|
|
204
|
-
return {
|
|
205
|
-
...resolved,
|
|
206
|
-
overridesProject: value?.overridesProject ?? differsFromDefault,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function configPath(workspaceRoot: string): string {
|
|
211
|
-
return join(sandboxDirectory(workspaceRoot), 'config.json');
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function keyPath(workspaceRoot: string): string {
|
|
215
|
-
return join(sandboxDirectory(workspaceRoot), 'secret.key');
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function unsafeLocalPath(path: string): SandboxSetupError {
|
|
219
|
-
return new SandboxSetupError(
|
|
220
|
-
'UNSAFE_SANDBOX_CONFIG_PATH',
|
|
221
|
-
`The sandbox local path ${path} must be regular and must not be a symbolic link.`,
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
async function assertSafeLocalPath(
|
|
226
|
-
workspaceRoot: string,
|
|
227
|
-
path: string,
|
|
228
|
-
createDirectory: boolean,
|
|
229
|
-
): Promise<void> {
|
|
230
|
-
for (const directory of [
|
|
231
|
-
flowdularStateDirectory(workspaceRoot),
|
|
232
|
-
sandboxDirectory(workspaceRoot),
|
|
233
|
-
]) {
|
|
234
|
-
try {
|
|
235
|
-
const info = await lstat(directory);
|
|
236
|
-
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
237
|
-
throw unsafeLocalPath(directory);
|
|
238
|
-
}
|
|
239
|
-
await chmod(directory, 0o700);
|
|
240
|
-
} catch (error) {
|
|
241
|
-
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
242
|
-
if (!createDirectory) return;
|
|
243
|
-
await mkdir(directory, { mode: 0o700 });
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
try {
|
|
247
|
-
const info = await lstat(path);
|
|
248
|
-
if (info.isSymbolicLink() || !info.isFile()) throw unsafeLocalPath(path);
|
|
249
|
-
} catch (error) {
|
|
250
|
-
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async function readLocalFile(path: string): Promise<string> {
|
|
255
|
-
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
256
|
-
try {
|
|
257
|
-
await handle.chmod(0o600);
|
|
258
|
-
return await handle.readFile('utf8');
|
|
259
|
-
} finally {
|
|
260
|
-
await handle.close();
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
async function writeLocalFile(path: string, value: string): Promise<void> {
|
|
265
|
-
const handle = await open(
|
|
266
|
-
path,
|
|
267
|
-
constants.O_WRONLY |
|
|
268
|
-
constants.O_CREAT |
|
|
269
|
-
constants.O_TRUNC |
|
|
270
|
-
constants.O_NOFOLLOW,
|
|
271
|
-
0o600,
|
|
272
|
-
);
|
|
273
|
-
try {
|
|
274
|
-
await handle.chmod(0o600);
|
|
275
|
-
await handle.writeFile(value, 'utf8');
|
|
276
|
-
} finally {
|
|
277
|
-
await handle.close();
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/* Secrets stay on the machine that runs the sandbox: encrypted at rest with a
|
|
282
|
-
permission-restricted local key, never written to the workspace repository
|
|
283
|
-
and never returned to the browser. */
|
|
284
|
-
async function localKey(workspaceRoot: string): Promise<Buffer> {
|
|
285
|
-
const path = keyPath(workspaceRoot);
|
|
286
|
-
try {
|
|
287
|
-
await assertSafeLocalPath(workspaceRoot, path, false);
|
|
288
|
-
const encoded = await readLocalFile(path);
|
|
289
|
-
const key = Buffer.from(encoded.trim(), 'base64');
|
|
290
|
-
if (key.byteLength === 32) return key;
|
|
291
|
-
} catch (error) {
|
|
292
|
-
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
293
|
-
}
|
|
294
|
-
await assertSafeLocalPath(workspaceRoot, path, true);
|
|
295
|
-
const key = randomBytes(32);
|
|
296
|
-
await writeLocalFile(path, key.toString('base64'));
|
|
297
|
-
return key;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
export async function sealSecret(
|
|
301
|
-
workspaceRoot: string,
|
|
302
|
-
value: string,
|
|
303
|
-
): Promise<SealedSecret> {
|
|
304
|
-
const key = await localKey(workspaceRoot);
|
|
305
|
-
const iv = randomBytes(12);
|
|
306
|
-
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
307
|
-
const ciphertext = Buffer.concat([
|
|
308
|
-
cipher.update(value, 'utf8'),
|
|
309
|
-
cipher.final(),
|
|
310
|
-
]);
|
|
311
|
-
return {
|
|
312
|
-
iv: iv.toString('base64'),
|
|
313
|
-
tag: cipher.getAuthTag().toString('base64'),
|
|
314
|
-
ciphertext: ciphertext.toString('base64'),
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
export async function openSecret(
|
|
319
|
-
workspaceRoot: string,
|
|
320
|
-
sealed: SealedSecret,
|
|
321
|
-
): Promise<string> {
|
|
322
|
-
const key = await localKey(workspaceRoot);
|
|
323
|
-
const decipher = createDecipheriv(
|
|
324
|
-
'aes-256-gcm',
|
|
325
|
-
key,
|
|
326
|
-
Buffer.from(sealed.iv, 'base64'),
|
|
327
|
-
);
|
|
328
|
-
decipher.setAuthTag(Buffer.from(sealed.tag, 'base64'));
|
|
329
|
-
return Buffer.concat([
|
|
330
|
-
decipher.update(Buffer.from(sealed.ciphertext, 'base64')),
|
|
331
|
-
decipher.final(),
|
|
332
|
-
]).toString('utf8');
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
export function secretFingerprint(sealed: SealedSecret | null): string | null {
|
|
336
|
-
return sealed
|
|
337
|
-
? createHash('sha256').update(sealed.ciphertext).digest('hex').slice(0, 8)
|
|
338
|
-
: null;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
export function assertPlatformUrl(value: string): string {
|
|
342
|
-
let url: URL;
|
|
343
|
-
try {
|
|
344
|
-
url = new URL(value);
|
|
345
|
-
} catch {
|
|
346
|
-
throw new SandboxSetupError(
|
|
347
|
-
'INVALID_PLATFORM_URL',
|
|
348
|
-
'The platform address must be an absolute URL.',
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
352
|
-
throw new SandboxSetupError(
|
|
353
|
-
'INVALID_PLATFORM_URL',
|
|
354
|
-
'The platform address must use http or https.',
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
const loopback =
|
|
358
|
-
url.hostname === 'localhost' ||
|
|
359
|
-
url.hostname === '::1' ||
|
|
360
|
-
url.hostname === '[::1]' ||
|
|
361
|
-
/^127(?:\.\d{1,3}){3}$/.test(url.hostname);
|
|
362
|
-
if (url.protocol !== 'https:' && !loopback) {
|
|
363
|
-
throw new SandboxSetupError(
|
|
364
|
-
'INVALID_PLATFORM_URL',
|
|
365
|
-
'The platform address must use HTTPS unless it is a loopback address.',
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
return url.origin;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/* The launcher decides the mode from the interface it actually bound, so a
|
|
372
|
-
stored configuration can never grant loopback trust to a public listener. */
|
|
373
|
-
function modeFromEnvironment(stored: SandboxRuntimeMode): SandboxRuntimeMode {
|
|
374
|
-
const value = flowdularEnvironment(process.env).FD_SANDBOX_MODE;
|
|
375
|
-
if (value === 'loopback' || value === 'self-hosted') return value;
|
|
376
|
-
return stored;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
export async function loadSandboxConfiguration(
|
|
380
|
-
workspaceRoot: string,
|
|
381
|
-
): Promise<SandboxConfiguration> {
|
|
382
|
-
try {
|
|
383
|
-
await assertSafeLocalPath(workspaceRoot, configPath(workspaceRoot), false);
|
|
384
|
-
const stored = JSON.parse(
|
|
385
|
-
await readLocalFile(configPath(workspaceRoot)),
|
|
386
|
-
) as Partial<SandboxConfiguration>;
|
|
387
|
-
return {
|
|
388
|
-
...DEFAULT_CONFIGURATION,
|
|
389
|
-
...stored,
|
|
390
|
-
version: 1,
|
|
391
|
-
mode: modeFromEnvironment(stored.mode ?? DEFAULT_CONFIGURATION.mode),
|
|
392
|
-
platformUrl: assertPlatformUrl(
|
|
393
|
-
stored.platformUrl ?? DEFAULT_CONFIGURATION.platformUrl,
|
|
394
|
-
),
|
|
395
|
-
github: resolveGitHubConfiguration(stored.github),
|
|
396
|
-
};
|
|
397
|
-
} catch (error) {
|
|
398
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
399
|
-
return {
|
|
400
|
-
...DEFAULT_CONFIGURATION,
|
|
401
|
-
mode: modeFromEnvironment(DEFAULT_CONFIGURATION.mode),
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
throw error;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
export async function saveSandboxConfiguration(
|
|
409
|
-
workspaceRoot: string,
|
|
410
|
-
configuration: SandboxConfiguration,
|
|
411
|
-
): Promise<SandboxConfiguration> {
|
|
412
|
-
const path = configPath(workspaceRoot);
|
|
413
|
-
await assertSafeLocalPath(workspaceRoot, path, true);
|
|
414
|
-
await writeLocalFile(path, `${JSON.stringify(configuration, null, '\t')}\n`);
|
|
415
|
-
return configuration;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
export interface SafeSandboxConfiguration {
|
|
419
|
-
readonly mode: SandboxRuntimeMode;
|
|
420
|
-
readonly platformUrl: string;
|
|
421
|
-
readonly platformTokenFingerprint: string | null;
|
|
422
|
-
readonly driver: string;
|
|
423
|
-
readonly driverModel: string | null;
|
|
424
|
-
readonly previewData: PreviewDataMode;
|
|
425
|
-
readonly byok: {
|
|
426
|
-
readonly kind: AiProviderKind;
|
|
427
|
-
readonly model: string;
|
|
428
|
-
readonly credentialFingerprint: string | null;
|
|
429
|
-
} | null;
|
|
430
|
-
readonly github: GitHubDeliveryConfiguration & {
|
|
431
|
-
readonly tokenFingerprint: string | null;
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/* What the browser is allowed to see. Credentials never leave the server. */
|
|
436
|
-
export function safeConfiguration(
|
|
437
|
-
configuration: SandboxConfiguration,
|
|
438
|
-
): SafeSandboxConfiguration {
|
|
439
|
-
return {
|
|
440
|
-
mode: configuration.mode,
|
|
441
|
-
platformUrl: configuration.platformUrl,
|
|
442
|
-
platformTokenFingerprint: secretFingerprint(configuration.platformToken),
|
|
443
|
-
driver: configuration.driver,
|
|
444
|
-
driverModel: configuration.driverModel,
|
|
445
|
-
previewData: configuration.previewData,
|
|
446
|
-
byok: configuration.byok
|
|
447
|
-
? {
|
|
448
|
-
kind: configuration.byok.kind,
|
|
449
|
-
model: configuration.byok.model,
|
|
450
|
-
credentialFingerprint: secretFingerprint(
|
|
451
|
-
configuration.byok.credential,
|
|
452
|
-
),
|
|
453
|
-
}
|
|
454
|
-
: null,
|
|
455
|
-
github: {
|
|
456
|
-
...configuration.github,
|
|
457
|
-
tokenFingerprint: secretFingerprint(configuration.gitProviderToken),
|
|
458
|
-
},
|
|
459
|
-
};
|
|
460
|
-
}
|
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
import { createReadStream } from 'node:fs';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { createInterface } from 'node:readline';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { readDeliveryRecord } from './delivery/record.ts';
|
|
6
|
-
import { ownsSession } from './session-owner.ts';
|
|
7
|
-
import { hashSpec } from './spec.ts';
|
|
8
|
-
import {
|
|
9
|
-
modulePathOf,
|
|
10
|
-
sessionPaths,
|
|
11
|
-
type ChatEntry,
|
|
12
|
-
type SandboxSession,
|
|
13
|
-
type SessionOwner,
|
|
14
|
-
} from './sessions.ts';
|
|
15
|
-
|
|
16
|
-
export type WorkStatus =
|
|
17
|
-
| 'draft'
|
|
18
|
-
| 'working'
|
|
19
|
-
| 'approval'
|
|
20
|
-
| 'approved'
|
|
21
|
-
| 'changes'
|
|
22
|
-
| 'rejected'
|
|
23
|
-
| 'attention'
|
|
24
|
-
| 'review'
|
|
25
|
-
| 'delivered'
|
|
26
|
-
| 'archived';
|
|
27
|
-
|
|
28
|
-
export interface WorkUsage {
|
|
29
|
-
inputTokens: number;
|
|
30
|
-
outputTokens: number;
|
|
31
|
-
totalTokens: number;
|
|
32
|
-
reportedTurns: number;
|
|
33
|
-
missingUsage: number;
|
|
34
|
-
knownCostUsd: number;
|
|
35
|
-
pricedTurns: number;
|
|
36
|
-
unpricedTurns: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface DashboardRow {
|
|
40
|
-
readonly session: SandboxSession;
|
|
41
|
-
readonly personal: boolean;
|
|
42
|
-
readonly status: WorkStatus;
|
|
43
|
-
readonly usage: WorkUsage;
|
|
44
|
-
readonly approvedModules: number;
|
|
45
|
-
readonly phases: readonly {
|
|
46
|
-
readonly role: string;
|
|
47
|
-
readonly usage: WorkUsage;
|
|
48
|
-
}[];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface SandboxDashboard {
|
|
52
|
-
readonly rows: readonly DashboardRow[];
|
|
53
|
-
readonly usage: WorkUsage;
|
|
54
|
-
readonly counts: Readonly<Record<WorkStatus, number>>;
|
|
55
|
-
readonly legacySessions: number;
|
|
56
|
-
readonly deletedSessions: number;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function emptyUsage(): WorkUsage {
|
|
60
|
-
return {
|
|
61
|
-
inputTokens: 0,
|
|
62
|
-
outputTokens: 0,
|
|
63
|
-
totalTokens: 0,
|
|
64
|
-
reportedTurns: 0,
|
|
65
|
-
missingUsage: 0,
|
|
66
|
-
knownCostUsd: 0,
|
|
67
|
-
pricedTurns: 0,
|
|
68
|
-
unpricedTurns: 0,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function addUsage(target: WorkUsage, source: WorkUsage): void {
|
|
73
|
-
for (const key of Object.keys(target) as (keyof WorkUsage)[])
|
|
74
|
-
target[key] += source[key];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/* A streaming projection, not another billing ledger. Keep only counters and
|
|
78
|
-
the latest decision, never prompts or tool output. Monotonic sequence ids
|
|
79
|
-
make duplicate transcript lines harmless without an unbounded id set. */
|
|
80
|
-
export async function summarizeTranscript(
|
|
81
|
-
entries: AsyncIterable<ChatEntry> | Iterable<ChatEntry>,
|
|
82
|
-
) {
|
|
83
|
-
const usage = emptyUsage();
|
|
84
|
-
const phases = new Map<string, WorkUsage>();
|
|
85
|
-
const decisions = new Map<string, ChatEntry['decision']>();
|
|
86
|
-
let sequence = 0;
|
|
87
|
-
let pending: WorkUsage | null = null;
|
|
88
|
-
for await (const entry of entries) {
|
|
89
|
-
if (entry.sequence <= sequence) continue;
|
|
90
|
-
sequence = entry.sequence;
|
|
91
|
-
if (entry.decision && entry.module)
|
|
92
|
-
decisions.set(entry.module, entry.decision);
|
|
93
|
-
const event = entry.event;
|
|
94
|
-
if (event?.type !== 'turn.started' && event?.type !== 'turn.completed')
|
|
95
|
-
continue;
|
|
96
|
-
const role = entry.role || 'unknown';
|
|
97
|
-
let phase = phases.get(role);
|
|
98
|
-
if (!phase) {
|
|
99
|
-
phase = emptyUsage();
|
|
100
|
-
phases.set(role, phase);
|
|
101
|
-
}
|
|
102
|
-
if (event.type === 'turn.started') {
|
|
103
|
-
if (pending) {
|
|
104
|
-
pending.missingUsage++;
|
|
105
|
-
pending.unpricedTurns++;
|
|
106
|
-
}
|
|
107
|
-
pending = phase;
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
pending = null;
|
|
111
|
-
const values = [
|
|
112
|
-
event.usage?.inputTokens,
|
|
113
|
-
event.usage?.outputTokens,
|
|
114
|
-
event.usage?.totalTokens,
|
|
115
|
-
];
|
|
116
|
-
const valid =
|
|
117
|
-
values.every((value) => Number.isSafeInteger(value) && value >= 0) &&
|
|
118
|
-
(event.finishReason === 'stop' || values.some((value) => value > 0));
|
|
119
|
-
if (valid) {
|
|
120
|
-
phase.inputTokens += event.usage.inputTokens;
|
|
121
|
-
phase.outputTokens += event.usage.outputTokens;
|
|
122
|
-
phase.totalTokens += event.usage.totalTokens;
|
|
123
|
-
phase.reportedTurns++;
|
|
124
|
-
} else phase.missingUsage++;
|
|
125
|
-
if (
|
|
126
|
-
typeof event.costUsd === 'number' &&
|
|
127
|
-
Number.isFinite(event.costUsd) &&
|
|
128
|
-
event.costUsd >= 0
|
|
129
|
-
) {
|
|
130
|
-
phase.knownCostUsd += event.costUsd;
|
|
131
|
-
phase.pricedTurns++;
|
|
132
|
-
} else phase.unpricedTurns++;
|
|
133
|
-
}
|
|
134
|
-
if (pending) {
|
|
135
|
-
pending.missingUsage++;
|
|
136
|
-
pending.unpricedTurns++;
|
|
137
|
-
}
|
|
138
|
-
for (const phase of phases.values()) addUsage(usage, phase);
|
|
139
|
-
return {
|
|
140
|
-
usage,
|
|
141
|
-
phases: [...phases].map(([role, usage]) => ({ role, usage })),
|
|
142
|
-
decisions,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function* transcript(path: string): AsyncIterable<ChatEntry> {
|
|
147
|
-
const stream = createReadStream(path, { encoding: 'utf8' });
|
|
148
|
-
const lines = createInterface({ input: stream, crlfDelay: Infinity });
|
|
149
|
-
try {
|
|
150
|
-
for await (const line of lines) {
|
|
151
|
-
if (!line.trim()) continue;
|
|
152
|
-
try {
|
|
153
|
-
yield JSON.parse(line) as ChatEntry;
|
|
154
|
-
} catch {
|
|
155
|
-
throw new Error(
|
|
156
|
-
'The session history is incomplete. Reload the dashboard.',
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
} catch (error) {
|
|
161
|
-
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
162
|
-
} finally {
|
|
163
|
-
lines.close();
|
|
164
|
-
stream.destroy();
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export async function buildDashboard(
|
|
169
|
-
root: string,
|
|
170
|
-
sessions: readonly SandboxSession[],
|
|
171
|
-
owner: SessionOwner | null,
|
|
172
|
-
running: ReadonlySet<string>,
|
|
173
|
-
): Promise<SandboxDashboard> {
|
|
174
|
-
const usage = emptyUsage();
|
|
175
|
-
const rows: DashboardRow[] = [];
|
|
176
|
-
const counts: Record<WorkStatus, number> = {
|
|
177
|
-
draft: 0,
|
|
178
|
-
working: 0,
|
|
179
|
-
approval: 0,
|
|
180
|
-
approved: 0,
|
|
181
|
-
changes: 0,
|
|
182
|
-
rejected: 0,
|
|
183
|
-
attention: 0,
|
|
184
|
-
review: 0,
|
|
185
|
-
delivered: 0,
|
|
186
|
-
archived: 0,
|
|
187
|
-
};
|
|
188
|
-
let legacySessions = 0;
|
|
189
|
-
let deletedSessions = 0;
|
|
190
|
-
for (const session of sessions) {
|
|
191
|
-
const personal = ownsSession(session, owner);
|
|
192
|
-
const paths = sessionPaths(root, session.id, session.moduleSuffix);
|
|
193
|
-
const summary = await summarizeTranscript(transcript(paths.chatLog));
|
|
194
|
-
if (personal) addUsage(usage, summary.usage);
|
|
195
|
-
else legacySessions++;
|
|
196
|
-
if (session.state === 'deleted') {
|
|
197
|
-
if (personal) deletedSessions++;
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
let approvedModules = 0;
|
|
201
|
-
for (const module of session.modules) {
|
|
202
|
-
if (!module.specHash || !module.specApprovedAt) continue;
|
|
203
|
-
const spec = await readFile(
|
|
204
|
-
join(modulePathOf(paths, module.directory), 'spec/module.yaml'),
|
|
205
|
-
'utf8',
|
|
206
|
-
).catch(() => null);
|
|
207
|
-
if (spec && hashSpec(spec) === module.specHash) approvedModules++;
|
|
208
|
-
}
|
|
209
|
-
const delivery = session.ejectedAt
|
|
210
|
-
? await readDeliveryRecord(paths.root)
|
|
211
|
-
: null;
|
|
212
|
-
const status: WorkStatus = session.rejectedAt
|
|
213
|
-
? 'rejected'
|
|
214
|
-
: session.archivedAt
|
|
215
|
-
? 'archived'
|
|
216
|
-
: session.ejectedAt
|
|
217
|
-
? delivery?.target === 'workspace'
|
|
218
|
-
? 'delivered'
|
|
219
|
-
: 'review'
|
|
220
|
-
: running.has(session.id)
|
|
221
|
-
? 'working'
|
|
222
|
-
: session.state === 'failed' || session.state === 'blocked'
|
|
223
|
-
? 'attention'
|
|
224
|
-
: session.state === 'awaiting-approval'
|
|
225
|
-
? 'approval'
|
|
226
|
-
: approvedModules === session.modules.length &&
|
|
227
|
-
approvedModules > 0
|
|
228
|
-
? 'approved'
|
|
229
|
-
: [...summary.decisions.values()].includes(
|
|
230
|
-
'changes-requested',
|
|
231
|
-
)
|
|
232
|
-
? 'changes'
|
|
233
|
-
: 'draft';
|
|
234
|
-
if (personal) counts[status]++;
|
|
235
|
-
rows.push({
|
|
236
|
-
session,
|
|
237
|
-
personal,
|
|
238
|
-
status,
|
|
239
|
-
usage: summary.usage,
|
|
240
|
-
phases: summary.phases,
|
|
241
|
-
approvedModules,
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
return { rows, usage, counts, legacySessions, deletedSessions };
|
|
245
|
-
}
|