forgeos 0.1.0-alpha.32 → 0.1.0-alpha.34

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 (38) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +23 -0
  3. package/docs/changelog.md +33 -0
  4. package/package.json +1 -1
  5. package/src/forge/_generated/releaseManifest.json +1 -1
  6. package/src/forge/_generated/releaseManifest.ts +3 -3
  7. package/src/forge/cli/changed.ts +102 -8
  8. package/src/forge/cli/commands.ts +116 -4
  9. package/src/forge/cli/dev.ts +228 -0
  10. package/src/forge/cli/handoff.ts +17 -0
  11. package/src/forge/cli/main.ts +36 -0
  12. package/src/forge/cli/new.ts +4 -0
  13. package/src/forge/cli/parse.ts +24 -6
  14. package/src/forge/cli/workos.ts +250 -23
  15. package/src/forge/compiler/app-graph/build.ts +44 -1
  16. package/src/forge/compiler/client-sdk/render-client.ts +31 -0
  17. package/src/forge/compiler/data-graph/parse.ts +17 -1
  18. package/src/forge/compiler/data-graph/sql/ddl.ts +24 -13
  19. package/src/forge/compiler/diagnostics/codes.ts +6 -0
  20. package/src/forge/compiler/diagnostics/create.ts +14 -0
  21. package/src/forge/compiler/integration/plan.ts +6 -0
  22. package/src/forge/compiler/integration/render.ts +9 -0
  23. package/src/forge/compiler/integration/templates/render.ts +2 -0
  24. package/src/forge/compiler/integration/templates/types.ts +3 -0
  25. package/src/forge/compiler/integration/templates/workos.ts +165 -99
  26. package/src/forge/compiler/orchestrator/plan.ts +7 -0
  27. package/src/forge/delta/status.ts +10 -1
  28. package/src/forge/impact/index.ts +158 -1
  29. package/src/forge/impact/types.ts +27 -1
  30. package/src/forge/react/index.ts +19 -2
  31. package/src/forge/runtime/auth/resolve.ts +33 -3
  32. package/src/forge/runtime/auth/types.ts +1 -0
  33. package/src/forge/server.ts +5 -1
  34. package/src/forge/version.ts +1 -1
  35. package/src/forge/vue/index.ts +16 -1
  36. package/src/forge/workspace/git-summary.ts +1 -1
  37. package/templates/nuxt-web/web/package.json +2 -1
  38. package/templates/nuxt-web/web/tsconfig.json +4 -1
@@ -191,6 +191,8 @@ export function buildIntegrationEmitPlan(input: IntegrationPlanInput): EmitPlan
191
191
  secrets,
192
192
  compatible,
193
193
  incompatible,
194
+ workspaceRoot: ctx.workspaceRoot,
195
+ appGraph,
194
196
  });
195
197
 
196
198
  const files: EmitFile[] = [];
@@ -212,6 +214,8 @@ export function buildIntegrationEmitPlan(input: IntegrationPlanInput): EmitPlan
212
214
  secrets,
213
215
  compatible,
214
216
  incompatible,
217
+ workspaceRoot: ctx.workspaceRoot,
218
+ appGraph,
215
219
  }),
216
220
  ),
217
221
  );
@@ -256,6 +260,8 @@ export function buildIntegrationEmitPlan(input: IntegrationPlanInput): EmitPlan
256
260
  secrets,
257
261
  compatible,
258
262
  incompatible,
263
+ workspaceRoot: ctx.workspaceRoot,
264
+ appGraph,
259
265
  }),
260
266
  ),
261
267
  );
@@ -1,4 +1,5 @@
1
1
  import type { SecretRequirement } from "../types/capability.ts";
2
+ import type { AppGraph } from "../types/app-graph.ts";
2
3
  import type { IntegrationRecipe } from "../types/integration.ts";
3
4
  import type { RuntimeContext } from "../types/runtime.ts";
4
5
  import { RUNTIME_CONTEXTS } from "../types/runtime.ts";
@@ -93,6 +94,8 @@ export function createRenderContext(input: {
93
94
  secrets: SecretRequirement[];
94
95
  compatible: RuntimeContext[];
95
96
  incompatible: RuntimeContext[];
97
+ workspaceRoot?: string;
98
+ appGraph?: AppGraph;
96
99
  }): IntegrationTemplateInput {
97
100
  return buildTemplateInput(input);
98
101
  }
@@ -106,6 +109,8 @@ export function renderAdapterModule(input: {
106
109
  secrets: SecretRequirement[];
107
110
  compatible: RuntimeContext[];
108
111
  incompatible: RuntimeContext[];
112
+ workspaceRoot?: string;
113
+ appGraph?: AppGraph;
109
114
  }): string {
110
115
  const ctx = createRenderContext(input);
111
116
  const adapterFilename = `${input.alias}.${input.context}.ts`;
@@ -168,6 +173,8 @@ export function renderIntegrationDoc(input: {
168
173
  secrets: SecretRequirement[];
169
174
  compatible: RuntimeContext[];
170
175
  incompatible: RuntimeContext[];
176
+ workspaceRoot?: string;
177
+ appGraph?: AppGraph;
171
178
  }): string {
172
179
  const ctx = buildTemplateInput({
173
180
  alias: input.alias,
@@ -178,6 +185,8 @@ export function renderIntegrationDoc(input: {
178
185
  secrets: input.secrets,
179
186
  compatible: input.compatible,
180
187
  incompatible: input.incompatible,
188
+ workspaceRoot: input.workspaceRoot,
189
+ appGraph: input.appGraph,
181
190
  });
182
191
 
183
192
  const docName = `${input.alias}.md`;
@@ -143,6 +143,8 @@ export function buildTemplateInput(input: {
143
143
  secrets: IntegrationTemplateInput["secrets"];
144
144
  compatible: IntegrationTemplateInput["compatible"];
145
145
  incompatible: IntegrationTemplateInput["incompatible"];
146
+ workspaceRoot?: string;
147
+ appGraph?: IntegrationTemplateInput["appGraph"];
146
148
  }): IntegrationTemplateInput {
147
149
  return input;
148
150
  }
@@ -1,4 +1,5 @@
1
1
  import type { SecretRequirement } from "../../types/capability.ts";
2
+ import type { AppGraph } from "../../types/app-graph.ts";
2
3
  import type { IntegrationRecipe } from "../../types/integration.ts";
3
4
  import type { RuntimeContext } from "../../types/runtime.ts";
4
5
 
@@ -11,4 +12,6 @@ export interface IntegrationTemplateInput {
11
12
  secrets: SecretRequirement[];
12
13
  compatible: RuntimeContext[];
13
14
  incompatible: RuntimeContext[];
15
+ workspaceRoot?: string;
16
+ appGraph?: AppGraph;
14
17
  }
@@ -1,8 +1,108 @@
1
1
  import type { IntegrationTemplateInput } from "./types.ts";
2
+ import { buildDataGraph } from "../../data-graph/build.ts";
2
3
 
3
4
  const SECRETS_IMPORT = 'import type { SecretsContext } from "../secretsContext.js";';
4
5
  const CONFIG_IMPORT = 'import type { ConfigContext } from "../secretsContext.js";';
5
6
 
7
+ const FALLBACK_PERMISSIONS = [
8
+ "onboarding:read",
9
+ "projects:manage",
10
+ "members:manage",
11
+ "invitations:create",
12
+ "tasks:manage",
13
+ "tasks:update",
14
+ ] as const;
15
+
16
+ const FALLBACK_RESOURCE_TYPES = [
17
+ { slug: "organization", name: "Organization" },
18
+ { slug: "project", name: "Project", parent: "organization" },
19
+ { slug: "team", name: "Team", parent: "organization" },
20
+ { slug: "taskGroup", name: "Task Group", parent: "project" },
21
+ { slug: "task", name: "Task", parent: "project" },
22
+ ] as const;
23
+
24
+ function quotedValues(text: string): string[] {
25
+ return [...text.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]!).filter(Boolean);
26
+ }
27
+
28
+ function uniqueSorted(values: Iterable<string>): string[] {
29
+ return [...new Set([...values].filter(Boolean))].sort();
30
+ }
31
+
32
+ function toTitle(value: string): string {
33
+ return value
34
+ .replace(/[:._-]+/g, " ")
35
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
36
+ .split(/\s+/)
37
+ .filter(Boolean)
38
+ .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
39
+ .join(" ");
40
+ }
41
+
42
+ function policyNameForPermission(permission: string): string {
43
+ return permission.replace(/:/g, ".").replace(/[^a-zA-Z0-9_.-]+/g, ".");
44
+ }
45
+
46
+ function singularResourceName(name: string): string {
47
+ if (name.endsWith("ies")) return `${name.slice(0, -3)}y`;
48
+ if (name.endsWith("ses")) return name.slice(0, -2);
49
+ if (name.endsWith("s") && name.length > 1) return name.slice(0, -1);
50
+ return name;
51
+ }
52
+
53
+ function permissionsFromApp(input: IntegrationTemplateInput): string[] {
54
+ const permissions = new Set<string>();
55
+ for (const symbol of input.appGraph?.symbols ?? []) {
56
+ const sourceSlice = typeof symbol.meta.sourceSlice === "string" ? symbol.meta.sourceSlice : "";
57
+ for (const match of sourceSlice.matchAll(/canPermission\s*\(([^)]*)\)/g)) {
58
+ for (const value of quotedValues(match[1] ?? "")) {
59
+ permissions.add(value);
60
+ }
61
+ }
62
+ }
63
+ return permissions.size > 0 ? uniqueSorted(permissions) : [...FALLBACK_PERMISSIONS];
64
+ }
65
+
66
+ function resourceTypesFromApp(input: IntegrationTemplateInput): Array<{ slug: string; name: string; parent?: string }> {
67
+ if (!input.appGraph) {
68
+ return [...FALLBACK_RESOURCE_TYPES];
69
+ }
70
+ const dataGraph = buildDataGraph(input.appGraph);
71
+ const tenantTables = dataGraph.tables.filter((table) =>
72
+ table.fields.some((field) => field.name === "tenantId")
73
+ );
74
+ if (tenantTables.length === 0) {
75
+ return [...FALLBACK_RESOURCE_TYPES];
76
+ }
77
+ const resourceTypes = new Map<string, { slug: string; name: string; parent?: string }>();
78
+ resourceTypes.set("organization", { slug: "organization", name: "Organization" });
79
+ for (const table of tenantTables) {
80
+ if (["organization", "organizations", "membership", "memberships"].includes(table.name)) {
81
+ continue;
82
+ }
83
+ const slug = singularResourceName(table.name);
84
+ resourceTypes.set(slug, { slug, name: toTitle(slug), parent: "organization" });
85
+ }
86
+ return [...resourceTypes.values()].sort((a, b) => a.slug.localeCompare(b.slug));
87
+ }
88
+
89
+ function readPermissions(permissions: string[]): string[] {
90
+ return permissions.filter((permission) => /(^|:)read$|read|list|view/i.test(permission));
91
+ }
92
+
93
+ function updatePermissions(permissions: string[]): string[] {
94
+ return permissions.filter((permission) => /update|progress|write/i.test(permission));
95
+ }
96
+
97
+ function rolePermissions(permissions: string[]): Record<string, string[]> {
98
+ const member = uniqueSorted([...readPermissions(permissions), ...updatePermissions(permissions)]);
99
+ return {
100
+ owner: permissions,
101
+ manager: permissions,
102
+ member: member.length > 0 ? member : permissions.slice(0, 1),
103
+ };
104
+ }
105
+
6
106
  export function renderWorkosServerAdapter(_input: IntegrationTemplateInput): string {
7
107
  return [
8
108
  "/** Forge generated WorkOS server adapter — action/workflow/endpoint/server contexts only. */",
@@ -110,7 +210,8 @@ export function renderWorkosAuthkit(_input: IntegrationTemplateInput): string {
110
210
  ].join("\n");
111
211
  }
112
212
 
113
- export function renderWorkosPolicies(_input: IntegrationTemplateInput): string {
213
+ export function renderWorkosPolicies(input: IntegrationTemplateInput): string {
214
+ const permissions = permissionsFromApp(input);
114
215
  return [
115
216
  "/** Forge generated WorkOS-derived policy template.",
116
217
  " * Import or merge these policies into src/policies.ts after reviewing names against your app.",
@@ -118,21 +219,15 @@ export function renderWorkosPolicies(_input: IntegrationTemplateInput): string {
118
219
  'import { canPermission, definePolicies } from "forge/policy";',
119
220
  "",
120
221
  "export const workosPermissionPolicies = definePolicies({",
121
- ' "onboarding.read": canPermission("onboarding:read"),',
122
- ' "projects.manage": canPermission("projects:manage"),',
123
- ' "members.manage": canPermission("members:manage"),',
124
- ' "invitations.create": canPermission("invitations:create"),',
125
- ' "tasks.manage": canPermission("tasks:manage"),',
126
- ' "tasks.update": canPermission("tasks:update"),',
222
+ ...permissions.map((permission) =>
223
+ ` "${policyNameForPermission(permission)}": canPermission("${permission}"),`
224
+ ),
127
225
  "});",
128
226
  "",
129
227
  "export const workosPermissionPolicyMap = {",
130
- ' "onboarding:read": "onboarding.read",',
131
- ' "projects:manage": "projects.manage",',
132
- ' "members:manage": "members.manage",',
133
- ' "invitations:create": "invitations.create",',
134
- ' "tasks:manage": "tasks.manage",',
135
- ' "tasks:update": "tasks.update",',
228
+ ...permissions.map((permission) =>
229
+ ` "${permission}": "${policyNameForPermission(permission)}",`
230
+ ),
136
231
  "} as const;",
137
232
  "",
138
233
  ].join("\n");
@@ -163,46 +258,29 @@ export function renderWorkosEnvExample(_input: IntegrationTemplateInput): string
163
258
  ].join("\n");
164
259
  }
165
260
 
166
- export function renderWorkosFga(_input: IntegrationTemplateInput): string {
261
+ export function renderWorkosFga(input: IntegrationTemplateInput): string {
262
+ const permissions = permissionsFromApp(input);
263
+ const roles = rolePermissions(permissions);
264
+ const resourceTypes = resourceTypesFromApp(input);
167
265
  return [
168
266
  "/** Forge generated WorkOS FGA bridge helpers. */",
169
267
  "",
170
268
  "export const forgeWorkOSDefaultPermissions = [",
171
- ' "onboarding:read",',
172
- ' "projects:manage",',
173
- ' "members:manage",',
174
- ' "invitations:create",',
175
- ' "tasks:manage",',
176
- ' "tasks:update",',
269
+ ...permissions.map((permission) => ` "${permission}",`),
177
270
  "] as const;",
178
271
  "",
179
272
  "export type ForgeWorkOSPermission = (typeof forgeWorkOSDefaultPermissions)[number];",
180
273
  "",
181
274
  "export const forgeWorkOSDefaultRoles = {",
182
- " owner: [",
183
- ' "onboarding:read",',
184
- ' "projects:manage",',
185
- ' "members:manage",',
186
- ' "invitations:create",',
187
- ' "tasks:manage",',
188
- ' "tasks:update",',
189
- " ],",
190
- " manager: [",
191
- ' "onboarding:read",',
192
- ' "members:manage",',
193
- ' "invitations:create",',
194
- ' "tasks:manage",',
195
- ' "tasks:update",',
196
- " ],",
197
- ' member: ["onboarding:read", "tasks:update"],',
275
+ ...Object.entries(roles).map(([role, rolePerms]) =>
276
+ ` ${role}: [${rolePerms.map((permission) => `"${permission}"`).join(", ")}],`
277
+ ),
198
278
  "} as const satisfies Record<string, readonly ForgeWorkOSPermission[]>;",
199
279
  "",
200
280
  "export const forgeWorkOSResourceTypes = [",
201
- ' { slug: "organization", description: "WorkOS organization / Forge tenant root" },',
202
- ' { slug: "project", parent: "organization", description: "Project under an organization" },',
203
- ' { slug: "team", parent: "organization", description: "Team under an organization" },',
204
- ' { slug: "taskGroup", parent: "project", description: "Task group under a project" },',
205
- ' { slug: "task", parent: "project", description: "Onboarding task under a project" },',
281
+ ...resourceTypes.map((resource) =>
282
+ ` { slug: "${resource.slug}",${resource.parent ? ` parent: "${resource.parent}",` : ""} description: "${resource.name} resource" },`
283
+ ),
206
284
  "] as const;",
207
285
  "",
208
286
  "export interface ForgeWorkOSAccessCheckInput {",
@@ -225,13 +303,23 @@ export function renderWorkosFga(_input: IntegrationTemplateInput): string {
225
303
  ].join("\n");
226
304
  }
227
305
 
228
- export function renderWorkosResourceMap(_input: IntegrationTemplateInput): string {
306
+ export function renderWorkosResourceMap(input: IntegrationTemplateInput): string {
307
+ const resourceTypes = resourceTypesFromApp(input);
308
+ const helperResourceKinds = uniqueSorted([
309
+ "organization",
310
+ ...resourceTypes.map((resource) => resource.slug),
311
+ "project",
312
+ "team",
313
+ "taskGroup",
314
+ "task",
315
+ ]);
316
+ const resourceKindUnion = helperResourceKinds.map((resource) => `"${resource}"`).join(" | ");
229
317
  return [
230
318
  "/** Forge generated WorkOS FGA resource bridge.",
231
319
  " * Keep calls to WorkOS Authorization API in server/action/workflow/endpoint contexts.",
232
320
  " */",
233
321
  "",
234
- "export type ForgeWorkOSResourceKind = \"organization\" | \"project\" | \"team\" | \"task\" | \"taskGroup\";",
322
+ `export type ForgeWorkOSResourceKind = ${resourceKindUnion};`,
235
323
  "export type ForgeWorkOSFgaFallback = \"deny\" | \"throw\";",
236
324
  "",
237
325
  "export interface ForgeWorkOSResourceRef {",
@@ -244,6 +332,7 @@ export function renderWorkosResourceMap(_input: IntegrationTemplateInput): strin
244
332
  "",
245
333
  "export interface ForgeWorkOSApplicationGraph {",
246
334
  " organization: ForgeWorkOSResourceRef;",
335
+ " resources: ForgeWorkOSResourceRef[];",
247
336
  " projects: ForgeWorkOSResourceRef[];",
248
337
  " teams: ForgeWorkOSResourceRef[];",
249
338
  " tasks: ForgeWorkOSResourceRef[];",
@@ -310,7 +399,8 @@ export function renderWorkosResourceMap(_input: IntegrationTemplateInput): strin
310
399
  " const parent = projectId ? projects.find((project) => project.externalId === workosResource(\"project\", projectId, organization).externalId) ?? organization : organization;",
311
400
  " return workosResource(\"task\", task.id, parent, task.name);",
312
401
  " });",
313
- " return { organization, projects, teams, tasks: [...taskGroups, ...tasks] };",
402
+ " const resources = [...projects, ...teams, ...taskGroups, ...tasks];",
403
+ " return { organization, resources, projects, teams, tasks: [...taskGroups, ...tasks] };",
314
404
  "}",
315
405
  "",
316
406
  "export interface ForgeWorkOSAuthorizationClient {",
@@ -362,7 +452,8 @@ export function renderWorkosResourceMap(_input: IntegrationTemplateInput): strin
362
452
  "",
363
453
  "export function workOSResourceRecords(input: { organizationId: string; graph: ForgeWorkOSApplicationGraph }): ForgeWorkOSResourceRecord[] {",
364
454
  " const organization = input.graph.organization;",
365
- " const resources = [...input.graph.projects, ...input.graph.teams, ...input.graph.tasks]",
455
+ " const legacyResources = [...(input.graph.projects ?? []), ...(input.graph.teams ?? []), ...(input.graph.tasks ?? [])];",
456
+ " const resources = [...(input.graph.resources ?? []), ...legacyResources]",
366
457
  " .flatMap((resource) => flattenResource(resource))",
367
458
  " .filter((resource) => resource.kind !== \"organization\");",
368
459
  " const byKey = new Map<string, ForgeWorkOSResourceRecord>();",
@@ -815,63 +906,33 @@ export function renderWorkosSeed(_input: IntegrationTemplateInput): string {
815
906
  ].join("\n");
816
907
  }
817
908
 
818
- export function renderWorkosSeedYaml(_input: IntegrationTemplateInput): string {
909
+ export function renderWorkosSeedYaml(input: IntegrationTemplateInput): string {
910
+ const permissions = permissionsFromApp(input);
911
+ const roles = rolePermissions(permissions);
912
+ const resourceTypes = resourceTypesFromApp(input);
819
913
  return [
820
914
  "# Forge generated WorkOS seed file.",
821
915
  "# Review names, domains, redirect URIs, and origins before applying to a real WorkOS environment.",
822
916
  "permissions:",
823
- " - name: 'Onboarding Read'",
824
- " slug: 'onboarding:read'",
825
- " - name: 'Projects Manage'",
826
- " slug: 'projects:manage'",
827
- " - name: 'Members Manage'",
828
- " slug: 'members:manage'",
829
- " - name: 'Invitations Create'",
830
- " slug: 'invitations:create'",
831
- " - name: 'Tasks Manage'",
832
- " slug: 'tasks:manage'",
833
- " - name: 'Tasks Update'",
834
- " slug: 'tasks:update'",
917
+ ...permissions.flatMap((permission) => [
918
+ ` - name: '${toTitle(permission)}'`,
919
+ ` slug: '${permission}'`,
920
+ ]),
835
921
  "",
836
922
  "resource_types:",
837
- " - slug: 'organization'",
838
- " name: 'Organization'",
839
- " - slug: 'project'",
840
- " name: 'Project'",
841
- " parent: 'organization'",
842
- " - slug: 'team'",
843
- " name: 'Team'",
844
- " parent: 'organization'",
845
- " - slug: 'taskGroup'",
846
- " name: 'Task Group'",
847
- " parent: 'project'",
848
- " - slug: 'task'",
849
- " name: 'Task'",
850
- " parent: 'project'",
923
+ ...resourceTypes.flatMap((resource) => [
924
+ ` - slug: '${resource.slug}'`,
925
+ ` name: '${resource.name}'`,
926
+ ...(resource.parent ? [` parent: '${resource.parent}'`] : []),
927
+ ]),
851
928
  "",
852
929
  "roles:",
853
- " - name: 'Owner'",
854
- " slug: 'owner'",
855
- " permissions:",
856
- " - 'onboarding:read'",
857
- " - 'projects:manage'",
858
- " - 'members:manage'",
859
- " - 'invitations:create'",
860
- " - 'tasks:manage'",
861
- " - 'tasks:update'",
862
- " - name: 'Manager'",
863
- " slug: 'manager'",
864
- " permissions:",
865
- " - 'onboarding:read'",
866
- " - 'members:manage'",
867
- " - 'invitations:create'",
868
- " - 'tasks:manage'",
869
- " - 'tasks:update'",
870
- " - name: 'Member'",
871
- " slug: 'member'",
872
- " permissions:",
873
- " - 'onboarding:read'",
874
- " - 'tasks:update'",
930
+ ...Object.entries(roles).flatMap(([role, rolePermissions]) => [
931
+ ` - name: '${toTitle(role)}'`,
932
+ ` slug: '${role}'`,
933
+ " permissions:",
934
+ ...rolePermissions.map((permission) => ` - '${permission}'`),
935
+ ]),
875
936
  "",
876
937
  "organizations:",
877
938
  " - name: 'Acme Corp'",
@@ -894,7 +955,12 @@ export function renderWorkosSeedYaml(_input: IntegrationTemplateInput): string {
894
955
  ].join("\n");
895
956
  }
896
957
 
897
- export function renderWorkosTestkit(_input: IntegrationTemplateInput): string {
958
+ export function renderWorkosTestkit(input: IntegrationTemplateInput): string {
959
+ const permissions = permissionsFromApp(input);
960
+ const roles = rolePermissions(permissions);
961
+ const ownerPermissions = JSON.stringify(roles.owner);
962
+ const managerPermissions = JSON.stringify(roles.manager);
963
+ const memberPermissions = JSON.stringify(roles.member);
898
964
  return [
899
965
  "/** Forge generated mock testkit for WorkOS auth and authorization. */",
900
966
  "",
@@ -907,9 +973,9 @@ export function renderWorkosTestkit(_input: IntegrationTemplateInput): string {
907
973
  " organizations.set(\"org_acme\", { id: \"org_acme\", name: \"Acme Corp\" });",
908
974
  " organizations.set(\"org_globex\", { id: \"org_globex\", name: \"Globex\" });",
909
975
  " const memberships = new Map<string, Membership>([",
910
- " [\"om_acme_owner\", { id: \"om_acme_owner\", organizationId: \"org_acme\", roleSlug: \"owner\", permissions: [\"onboarding:read\", \"projects:manage\", \"members:manage\", \"invitations:create\", \"tasks:manage\", \"tasks:update\"] }],",
911
- " [\"om_acme_member\", { id: \"om_acme_member\", organizationId: \"org_acme\", roleSlug: \"member\", permissions: [\"onboarding:read\", \"tasks:update\"] }],",
912
- " [\"om_globex_member\", { id: \"om_globex_member\", organizationId: \"org_globex\", roleSlug: \"member\", permissions: [\"onboarding:read\", \"tasks:update\"] }],",
976
+ ` ["om_acme_owner", { id: "om_acme_owner", organizationId: "org_acme", roleSlug: "owner", permissions: ${ownerPermissions} }],`,
977
+ ` ["om_acme_member", { id: "om_acme_member", organizationId: "org_acme", roleSlug: "member", permissions: ${memberPermissions} }],`,
978
+ ` ["om_globex_member", { id: "om_globex_member", organizationId: "org_globex", roleSlug: "member", permissions: ${memberPermissions} }],`,
913
979
  " ]);",
914
980
  " const resources = new Map<string, Resource>();",
915
981
  " const roleAssignments: RoleAssignment[] = [];",
@@ -934,7 +1000,7 @@ export function renderWorkosTestkit(_input: IntegrationTemplateInput): string {
934
1000
  " userManagement: {",
935
1001
  " createOrganizationMembership: async (input: { organizationId: string; roleSlug: string; userId?: string }) => {",
936
1002
  " const id = `om_${input.organizationId}_${memberships.size + 1}`;",
937
- " const permissions = input.roleSlug === \"owner\" ? [\"onboarding:read\", \"projects:manage\", \"members:manage\", \"invitations:create\", \"tasks:manage\", \"tasks:update\"] : input.roleSlug === \"manager\" ? [\"onboarding:read\", \"members:manage\", \"invitations:create\", \"tasks:manage\", \"tasks:update\"] : [\"onboarding:read\", \"tasks:update\"];",
1003
+ ` const permissions = input.roleSlug === "owner" ? ${ownerPermissions} : input.roleSlug === "manager" ? ${managerPermissions} : ${memberPermissions};`,
938
1004
  " const membership = { id, organizationId: input.organizationId, roleSlug: input.roleSlug, permissions };",
939
1005
  " memberships.set(id, membership);",
940
1006
  " return membership;",
@@ -1093,6 +1093,13 @@ export function plan(input: PlanInput): EmitPlan {
1093
1093
  orphanedFiles,
1094
1094
  lock,
1095
1095
  diagnostics: [
1096
+ ...dataGraph.diagnostics,
1097
+ ...sqlPlan.diagnostics,
1098
+ ...runtimeGraph.diagnostics,
1099
+ ...queryRegistry.diagnostics,
1100
+ ...liveQueryRegistry.diagnostics,
1101
+ ...policyRegistry.diagnostics,
1102
+ ...tenantScope.diagnostics,
1096
1103
  ...agentArtifacts.diagnostics,
1097
1104
  ...rlsArtifacts.diagnostics,
1098
1105
  ...frontendGraph.diagnostics.map((diagnostic) =>
@@ -632,7 +632,16 @@ async function runDeltaDoctorRaw(workspaceRoot: string): Promise<DeltaDoctorResu
632
632
  });
633
633
 
634
634
  const gitignore = readGitignore(workspaceRoot);
635
- const requiredGitignore = [".forge/delta/", ".forge/agent/*.ndjson", ".forge/studio"];
635
+ const requiredGitignore = [
636
+ ".codex/",
637
+ ".forge/delta/",
638
+ ".forge/agent/",
639
+ ".forge/agent/*.ndjson",
640
+ ".forge/agent/*.history",
641
+ ".forge/last-run.json",
642
+ ".forge/runtime-cache/",
643
+ ".forge/studio",
644
+ ];
636
645
  const missingGitignore = requiredGitignore.filter((entry) => !gitignore.includes(entry));
637
646
  checks.push({
638
647
  name: "gitignore-operational-state",