forgeos 0.1.0-alpha.30 → 0.1.0-alpha.32

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 (49) hide show
  1. package/AGENTS.md +3 -3
  2. package/CHANGELOG.md +30 -0
  3. package/docs/changelog.md +55 -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/agent-adapters/index.ts +28 -2
  8. package/src/forge/cli/auth.ts +134 -9
  9. package/src/forge/cli/baseline.ts +112 -0
  10. package/src/forge/cli/changed.ts +52 -15
  11. package/src/forge/cli/commands.ts +117 -31
  12. package/src/forge/cli/db.ts +92 -4
  13. package/src/forge/cli/dev.ts +229 -44
  14. package/src/forge/cli/doctor.ts +81 -0
  15. package/src/forge/cli/handoff.ts +63 -5
  16. package/src/forge/cli/last-run.ts +84 -0
  17. package/src/forge/cli/main.ts +3 -1
  18. package/src/forge/cli/new.ts +130 -12
  19. package/src/forge/cli/output.ts +94 -12
  20. package/src/forge/cli/parse.ts +72 -10
  21. package/src/forge/cli/progress.ts +51 -0
  22. package/src/forge/cli/studio.ts +15 -14
  23. package/src/forge/cli/verify.ts +31 -0
  24. package/src/forge/cli/windows.ts +23 -0
  25. package/src/forge/cli/workos.ts +5 -4
  26. package/src/forge/compiler/agent-contract/build.ts +3 -3
  27. package/src/forge/compiler/data-graph/sql/ddl.ts +11 -1
  28. package/src/forge/compiler/diagnostics/codes.ts +4 -0
  29. package/src/forge/compiler/integration/templates/render.ts +1 -0
  30. package/src/forge/compiler/integration/templates/workos.ts +5 -5
  31. package/src/forge/compiler/make-registry/build.ts +2 -2
  32. package/src/forge/compiler/orchestrator/generate-lock.ts +14 -3
  33. package/src/forge/compiler/recipes/definitions.ts +1 -1
  34. package/src/forge/delta/status.ts +79 -12
  35. package/src/forge/dev/server.ts +4 -4
  36. package/src/forge/dev-console/cycle.ts +55 -20
  37. package/src/forge/dev-console/types.ts +1 -0
  38. package/src/forge/make/fields.ts +26 -0
  39. package/src/forge/make/index.ts +6 -1
  40. package/src/forge/runtime/db/factory.ts +1 -3
  41. package/src/forge/runtime/db/memory-adapter.ts +139 -32
  42. package/src/forge/runtime/db/pglite-adapter.ts +188 -2
  43. package/src/forge/ui/index.ts +174 -0
  44. package/src/forge/ui/types.ts +1 -0
  45. package/src/forge/version.ts +1 -1
  46. package/src/forge/workspace/baseline.ts +112 -0
  47. package/src/forge/workspace/change-summary.ts +8 -1
  48. package/src/forge/workspace/forge-cli.ts +29 -1
  49. package/src/forge/workspace/git-summary.ts +65 -2
package/AGENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.30 input=5d12d4ad2a959d692b306eb18ba706c52f22f2d148c6e4ed541bd67fd8222c84 content=0d493cf0e41b71cb652d5e0e1b0c1f83d2a1281b748321f0b00f0773ba93074e
1
+ // @forge-generated generator=0.1.0-alpha.32 input=26469f4d84697cd1397a4a6726dff71ec50e70270a82b3ea4c84c75f25ac5f58 content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
2
2
  # AGENTS.md
3
3
 
4
4
  <!-- forge-generated:start -->
@@ -205,8 +205,8 @@ node bin/forge.mjs do verify --json
205
205
  Use:
206
206
 
207
207
  ```bash
208
- node bin/forge.mjs make resource <name> --fields title:text,status:enum(open,closed) --dry-run --json
209
- node bin/forge.mjs make resource <name> --fields title:text,status:enum(open,closed) --with-ui --yes
208
+ node bin/forge.mjs make resource <name> --fields title:text,status:enum=open+closed --dry-run --json
209
+ node bin/forge.mjs make resource <name> --fields title:text,status:enum=open+closed --with-ui --yes
210
210
  node bin/forge.mjs make ui --framework vite --dry-run --json
211
211
  node bin/forge.mjs make ui --framework nuxt --dry-run --json
212
212
  node bin/forge.mjs make ai-chat support --dry-run --json
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # forgeos
2
2
 
3
+ ## 0.1.0-alpha.32
4
+
5
+ ### Patch Changes
6
+
7
+ - Harden the alpha field-test loop after real Codex, WorkOS, PGlite, and app-server exercises.
8
+
9
+ - Accept `create-forgeos-app --git` as a supported/no-op compatibility flag when git initialization is already handled by the scaffold path, and keep `npm create forgeos-app@alpha .` working from empty current directories.
10
+ - Reject schema definitions that try to model `id` as a normal text field so generated SQL cannot silently create the wrong primary-key shape.
11
+ - Fix tenant-scoped updates in the in-memory database adapter and bring memory timestamp behavior closer to PGlite by rejecting empty timestamp strings and returning `Date` objects for timestamp columns.
12
+ - Improve smoke/generate drift diagnostics and keep `forge handoff` read-only with respect to generated artifacts.
13
+ - Copy generated WorkOS seed data to a root-level `workos-seed.yml` for app DX, serve `HEAD /auth.md` and `HEAD /.well-known/oauth-protected-resource`, and make local-vs-production auth posture more explicit.
14
+ - Normalize suggested Forge commands in framework checkouts so `agent`, `delta`, `studio`, `status`, `changed`, `handoff`, doctors, and WorkOS/auth helpers recommend `node bin/forge.mjs ...` instead of a possibly stale global `forge`.
15
+ - Return structured JSON diagnostics for Delta/PGlite export/open failures instead of leaking raw PGlite stack output, and improve Windows/PGlite repair guidance.
16
+ - Expand `forge ui audit` with static UX/auth-readiness warnings for missing semantic landmarks, unlabeled form controls, unnamed buttons, missing loading/error/empty states, and tenant/prod-auth apps that still only show local dev auth posture.
17
+
18
+ ## 0.1.0-alpha.31
19
+
20
+ ### Patch Changes
21
+
22
+ - Smooth the ForgeOS field-demo DX after the alpha.30 app-server and WorkOS exercises.
23
+
24
+ - Add `forge baseline create` and `forge baseline status` so non-git template workspaces can establish a local baseline and get useful `forge changed` / `forge handoff` diffs instead of noisy filesystem inventories.
25
+ - Add `forge auth status` and production-focused `forge auth prove --prod` output so `dev-headers` is clearly labeled as local-only while JWT/OIDC proofs show production auth posture.
26
+ - Expand `forge doctor windows` with local PGlite store posture and cleanup guidance, including safer PGlite abort inspection that does not poison the surrounding process exit code.
27
+ - Add `forge ui audit` and run it during `forge verify --smoke` when a web app is present, catching missing UI scenarios, missing stable Forge test IDs, and missing policy-denied coverage for sensitive routes.
28
+ - Expand `forge ui audit` with static UX/auth-readiness warnings for missing semantic landmarks, unlabeled form controls, unnamed buttons, missing loading/error/empty states, and tenant/prod-auth apps that still only show local dev auth posture.
29
+ - Bring the in-memory DB adapter closer to PGlite for timestamp fields by rejecting empty timestamp values and returning `Date` objects for `timestamp`/`timestamptz` columns.
30
+ - Make `forge handoff` use a read-only generated-artifact check so preparing a handoff no longer rewrites `src/forge/_generated/**`, `forge.lock`, or generated `AGENTS.md` noise; `forge dev --once` still self-heals stale artifacts.
31
+ - Allow `forge new .` / `npm create forgeos-app@alpha .` from an empty current directory, while refusing non-empty directories to avoid overwriting existing apps.
32
+
3
33
  ## 0.1.0-alpha.30
4
34
 
5
35
  ### Patch Changes
package/docs/changelog.md CHANGED
@@ -6,6 +6,61 @@ The canonical source file in the repository is `CHANGELOG.md`.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.1.0-alpha.32
10
+
11
+ - Accepted `create-forgeos-app --git` as a supported/no-op compatibility flag
12
+ when git initialization is already handled by the scaffold path, and kept
13
+ `npm create forgeos-app@alpha .` working from empty current directories.
14
+ - Added stronger schema validation for `id` so apps cannot accidentally model
15
+ the primary key as a normal text field and generate incorrect SQL.
16
+ - Fixed tenant-scoped updates in the in-memory database adapter and made memory
17
+ timestamp handling closer to PGlite by rejecting empty timestamp strings and
18
+ returning `Date` objects for timestamp columns.
19
+ - Improved smoke/generate drift diagnostics and kept `forge handoff` read-only
20
+ with respect to generated artifacts.
21
+ - Improved WorkOS/auth app DX: root-level `workos-seed.yml`, `HEAD /auth.md`,
22
+ `HEAD /.well-known/oauth-protected-resource`, and clearer local-vs-production
23
+ auth posture.
24
+ - Normalized suggested Forge commands in framework checkouts so `agent`,
25
+ `delta`, `studio`, `status`, `changed`, `handoff`, doctors, and WorkOS/auth
26
+ helpers recommend `node bin/forge.mjs ...` instead of a possibly stale global
27
+ `forge`.
28
+ - Returned structured JSON diagnostics for Delta/PGlite export/open failures
29
+ instead of raw PGlite stack output, and expanded Windows/PGlite repair hints.
30
+ - Expanded `forge ui audit` with static UX/auth-readiness warnings for missing
31
+ semantic landmarks, unlabeled form controls, unnamed buttons, missing
32
+ loading/error/empty states, and tenant/prod-auth apps that still only expose
33
+ local dev auth posture.
34
+
35
+ ## 0.1.0-alpha.31
36
+
37
+ - Added Forge workspace baselines for non-git app directories:
38
+ `forge baseline create` records a local baseline and `forge changed` /
39
+ `forge handoff` can now report baseline diffs instead of noisy full
40
+ filesystem inventories.
41
+ - Added `forge auth status` and production-aware `forge auth prove --prod`
42
+ posture checks so local `dev-headers` auth is clearly distinguished from
43
+ JWT/OIDC production authentication.
44
+ - Expanded `forge doctor windows` with local PGlite store posture and cleanup
45
+ guidance, and made PGlite abort inspection preserve the surrounding process
46
+ exit code.
47
+ - Added `forge ui audit` and wired it into `forge verify --smoke` for web apps
48
+ to catch missing route scenarios, missing stable Forge test IDs, and missing
49
+ policy-denied coverage for sensitive flows. It now also reports static
50
+ UX/auth-readiness warnings for missing semantic landmarks, unlabeled form
51
+ controls, unnamed buttons, missing loading/error/empty states, and
52
+ tenant/prod-auth apps that still only expose local dev auth posture.
53
+ - Brought the in-memory DB adapter closer to PGlite for timestamp fields by
54
+ rejecting empty timestamp values and returning `Date` objects for
55
+ `timestamp`/`timestamptz` columns.
56
+ - Made `forge handoff` use a read-only generated-artifact check so preparing a
57
+ handoff no longer rewrites `src/forge/_generated/**`, `forge.lock`, or
58
+ generated `AGENTS.md` noise; `forge dev --once` still self-heals stale
59
+ artifacts.
60
+ - Allowed `forge new .` / `npm create forgeos-app@alpha .` from an empty
61
+ current directory, while refusing non-empty directories to avoid overwriting
62
+ existing apps.
63
+
9
64
  ## 0.1.0-alpha.30
10
65
 
11
66
  - Hardened the WorkOS/AuthKit adapter and dev telemetry after the alpha.29
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgeos",
3
- "version": "0.1.0-alpha.30",
3
+ "version": "0.1.0-alpha.32",
4
4
  "description": "Agent-native application framework and compiler for building Forge apps without a mandatory dashboard.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1 +1 @@
1
- {"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.30","releaseId":"forgeos@0.1.0-alpha.30+unknown","schemaVersion":"0.1.0"}
1
+ {"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.32","releaseId":"forgeos@0.1.0-alpha.32+unknown","schemaVersion":"0.1.0"}
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.30 input=5d12d4ad2a959d692b306eb18ba706c52f22f2d148c6e4ed541bd67fd8222c84 content=065153708d210de0b5893a6e8c8ecbfc8bab3b011c8f1e576ae54d9c59bff838
1
+ // @forge-generated generator=0.1.0-alpha.32 input=26469f4d84697cd1397a4a6726dff71ec50e70270a82b3ea4c84c75f25ac5f58 content=ba652b73917e09bcc803cfea91f38e1aa2563b17eacc60f6faaebdc949997e9a
2
2
  export const releaseManifest = {
3
3
  "defaultProvider": "local",
4
4
  "diagnostics": [],
@@ -19,7 +19,7 @@ export const releaseManifest = {
19
19
  "custom"
20
20
  ],
21
21
  "packageName": "forgeos",
22
- "packageVersion": "0.1.0-alpha.30",
23
- "releaseId": "forgeos@0.1.0-alpha.30+unknown",
22
+ "packageVersion": "0.1.0-alpha.32",
23
+ "releaseId": "forgeos@0.1.0-alpha.32+unknown",
24
24
  "schemaVersion": "0.1.0"
25
25
  } as const;
@@ -56,6 +56,7 @@ import {
56
56
  } from "../agent-memory/hook-runner.ts";
57
57
  import { releaseManifest } from "../_generated/releaseManifest.ts";
58
58
  import { runDevConsoleCycle } from "../dev-console/cycle.ts";
59
+ import { normalizeForgeCliCommandsInValue } from "../workspace/forge-cli.ts";
59
60
 
60
61
  export const AGENT_ADAPTER_VERSION = "agent-adapter-0.1.0";
61
62
  export const AGENT_FORMAT_VERSION = "2026-06";
@@ -77,6 +78,25 @@ function diagnostic(
77
78
  return createDiagnostic({ severity, code, message, ...(file ? { file } : {}) });
78
79
  }
79
80
 
81
+ function normalizeAgentCliCommandHints<T>(workspaceRoot: string, result: T): T {
82
+ if (!result || typeof result !== "object") {
83
+ return result;
84
+ }
85
+ const value = result as Record<string, unknown>;
86
+ const normalized: Record<string, unknown> = { ...value };
87
+ for (const key of ["nextActions", "diagnostics", "commands", "checks", "steps"]) {
88
+ if (key in normalized) {
89
+ normalized[key] = normalizeForgeCliCommandsInValue(workspaceRoot, normalized[key], key);
90
+ }
91
+ }
92
+ for (const key of ["installResult", "ingestResult"]) {
93
+ if (key in normalized) {
94
+ normalized[key] = normalizeAgentCliCommandHints(workspaceRoot, normalized[key]);
95
+ }
96
+ }
97
+ return normalized as T;
98
+ }
99
+
80
100
  function readText(workspaceRoot: string, relative: string): string | null {
81
101
  const path = join(workspaceRoot, relative);
82
102
  if (!nodeFileSystem.exists(path)) {
@@ -2222,9 +2242,11 @@ export async function runAgentHooksSmoke(options: AgentCommandOptions): Promise<
2222
2242
  };
2223
2243
  }
2224
2244
 
2225
- export async function runAgentCommand(options: AgentCommandOptions): Promise<
2245
+ type AgentCommandResult =
2226
2246
  AgentExportResult | AgentCheckResult | AgentTargetsResult | AgentPrintContextResult | AgentDoctorResult | AgentPrepareResult | AgentOnboardResult | AgentHooksSmokeResult | AgentHooksStatusResult | AgentTimelineResult | AgentMemoryCommandResult
2227
- > {
2247
+ ;
2248
+
2249
+ async function runAgentCommandRaw(options: AgentCommandOptions): Promise<AgentCommandResult> {
2228
2250
  if (options.subcommand === "list-targets") {
2229
2251
  return runAgentListTargets(options.workspaceRoot);
2230
2252
  }
@@ -2295,6 +2317,10 @@ export async function runAgentCommand(options: AgentCommandOptions): Promise<
2295
2317
  };
2296
2318
  }
2297
2319
 
2320
+ export async function runAgentCommand(options: AgentCommandOptions): Promise<AgentCommandResult> {
2321
+ return normalizeAgentCliCommandHints(options.workspaceRoot, await runAgentCommandRaw(options));
2322
+ }
2323
+
2298
2324
  export function formatAgentJson(result: Awaited<ReturnType<typeof runAgentCommand>>): string {
2299
2325
  if ("timeline" in result && result.timeline === "agent") {
2300
2326
  return `${JSON.stringify(result, null, 2)}\n`;
@@ -9,14 +9,19 @@ import { mapClaimsToAuthContext } from "../runtime/auth/claims.ts";
9
9
  import { ForgeAuthError } from "../runtime/auth/errors.ts";
10
10
  import { verifyJwtToken } from "../runtime/auth/verifier.ts";
11
11
  import { loadSecretRegistry } from "../runtime/secrets/check.ts";
12
+ import { existsSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { normalizeForgeCliCommandsInValue } from "../workspace/forge-cli.ts";
12
15
 
13
- export type AuthSubcommand = "check" | "config" | "decode" | "test-token" | "jwks" | "prove";
16
+ export type AuthSubcommand = "check" | "config" | "decode" | "test-token" | "jwks" | "prove" | "status";
14
17
 
15
18
  export interface AuthCommandOptions {
16
19
  subcommand: AuthSubcommand;
17
20
  workspaceRoot: string;
18
21
  json: boolean;
19
22
  token?: string;
23
+ prod?: boolean;
24
+ scenario?: string;
20
25
  }
21
26
 
22
27
  export interface AuthCommandResult {
@@ -59,11 +64,8 @@ function detectWorkOS(workspaceRoot: string, claims: AuthClaimsMapping) {
59
64
  };
60
65
  }
61
66
 
62
- function validateConfig(workspaceRoot: string): AuthCommandResult {
63
- const config = loadAuthConfigFromEnv(workspaceRoot);
67
+ function configErrors(config: ReturnType<typeof loadAuthConfigFromEnv>): { code: string; message: string }[] {
64
68
  const errors: { code: string; message: string }[] = [];
65
- const workos = detectWorkOS(workspaceRoot, config.claims);
66
-
67
69
  if ((config.mode === "jwt" || config.mode === "oidc") && !config.issuer) {
68
70
  errors.push({
69
71
  code: FORGE_AUTH_INVALID_ISSUER,
@@ -82,6 +84,99 @@ function validateConfig(workspaceRoot: string): AuthCommandResult {
82
84
  message: "FORGE_AUTH_JWKS_URI is required for jwt auth",
83
85
  });
84
86
  }
87
+ return errors;
88
+ }
89
+
90
+ function buildAuthPosture(workspaceRoot: string) {
91
+ const config = loadAuthConfigFromEnv(workspaceRoot);
92
+ const productionMode = config.mode === "jwt" || config.mode === "oidc";
93
+ const errors = configErrors(config);
94
+ const configReady = errors.length === 0;
95
+ return normalizeForgeCliCommandsInValue(workspaceRoot, {
96
+ schemaVersion: "0.1.0",
97
+ mode: config.mode,
98
+ localOnly: config.mode === "dev-headers",
99
+ productionReady: productionMode && configReady,
100
+ requiresTenant: config.requiresTenant,
101
+ bearerHeader: productionMode ? "Authorization: Bearer <token>" : null,
102
+ tenantClaim: config.claims.tenantId ?? "tenant_id",
103
+ productionChecklist: [
104
+ { item: "auth mode is jwt or oidc", ok: productionMode },
105
+ { item: "FORGE_AUTH_ISSUER configured", ok: Boolean(config.issuer) },
106
+ { item: "FORGE_AUTH_AUDIENCE configured", ok: Boolean(config.audience) },
107
+ { item: "FORGE_AUTH_JWKS_URI or OIDC discovery configured", ok: config.mode === "oidc" || Boolean(config.jwksUri) },
108
+ { item: "tenant claim mapped", ok: Boolean(config.claims.tenantId) },
109
+ { item: "permission claim mapped", ok: Boolean(config.claims.permissions) },
110
+ { item: "dev-headers disabled for public runtime", ok: config.mode !== "dev-headers" },
111
+ ],
112
+ reason: productionMode
113
+ ? configReady
114
+ ? "jwt/oidc production auth configuration is present"
115
+ : "jwt/oidc production auth is selected but required settings are missing"
116
+ : "dev-headers auth is local-only and is not real production authentication",
117
+ nextActions: productionMode
118
+ ? configReady
119
+ ? ["forge auth prove --prod --token <jwt> --json", "forge serve --json"]
120
+ : ["forge auth check --json", "configure FORGE_AUTH_ISSUER, FORGE_AUTH_AUDIENCE, and FORGE_AUTH_JWKS_URI or OIDC issuer"]
121
+ : ["set FORGE_AUTH_MODE=jwt or oidc for production", "forge auth prove --prod --token <jwt> --json"],
122
+ });
123
+ }
124
+
125
+ function buildMultiTenantProof(workspaceRoot: string, workos: ReturnType<typeof detectWorkOS>, requiresTenant: boolean) {
126
+ const rootSeedPresent = existsSync(join(workspaceRoot, "workos-seed.yml"));
127
+ const generatedSeedPresent = existsSync(join(workspaceRoot, "src/forge/_generated/integrations/workos/workos-seed.yml"));
128
+ const authMdPresent = existsSync(join(workspaceRoot, "public/auth.md"));
129
+ const metadataPresent = existsSync(join(workspaceRoot, "public/.well-known/oauth-protected-resource"));
130
+ const permissions = ["onboarding:read", "invitations:create", "tasks:update"];
131
+ const permissionVocabularyPresent = workos.detected && workos.claimStatus.some((claim) => claim.name === "permissions" && claim.ok);
132
+ const checks = [
133
+ {
134
+ id: "tenant-claim",
135
+ ok: requiresTenant && workos.claimStatus.some((claim) => claim.name === "tenantId" && claim.ok),
136
+ evidence: "Forge tenant claim maps to WorkOS organization_id and tenant auth is required.",
137
+ },
138
+ {
139
+ id: "permission-claim",
140
+ ok: permissionVocabularyPresent,
141
+ evidence: "Forge permissions claim maps to WorkOS permissions.",
142
+ },
143
+ {
144
+ id: "seed-organizations",
145
+ ok: rootSeedPresent || generatedSeedPresent,
146
+ evidence: rootSeedPresent ? "workos-seed.yml exists at app root." : "generated WorkOS seed exists.",
147
+ },
148
+ {
149
+ id: "agent-auth-metadata",
150
+ ok: authMdPresent && metadataPresent,
151
+ evidence: "public/auth.md and protected resource metadata are present.",
152
+ },
153
+ ];
154
+ return normalizeForgeCliCommandsInValue(workspaceRoot, {
155
+ scenario: "multi-tenant",
156
+ ok: checks.every((check) => check.ok),
157
+ claims: {
158
+ acme: { organization_id: "org_acme", role: "owner", permissions },
159
+ globex: { organization_id: "org_globex", role: "member", permissions: ["onboarding:read", "tasks:update"] },
160
+ },
161
+ invariants: [
162
+ "Acme and Globex must use different organization_id claim values.",
163
+ "Tenant-scoped reads must include the active organization_id.",
164
+ "Tenant-scoped writes must verify the resource tenant before mutation.",
165
+ "Role-only UI affordances are not sufficient; policies must use permissions/claims.",
166
+ ],
167
+ checks,
168
+ nextActions: [
169
+ "forge workos doctor --json",
170
+ "forge workos seed --file workos-seed.yml --dry-run --json",
171
+ "run an HTTP E2E with Acme and Globex tokens before production",
172
+ ],
173
+ });
174
+ }
175
+
176
+ function validateConfig(workspaceRoot: string): AuthCommandResult {
177
+ const config = loadAuthConfigFromEnv(workspaceRoot);
178
+ const errors = configErrors(config);
179
+ const workos = detectWorkOS(workspaceRoot, config.claims);
85
180
 
86
181
  return {
87
182
  ok: errors.length === 0,
@@ -94,6 +189,7 @@ function validateConfig(workspaceRoot: string): AuthCommandResult {
94
189
  algorithms: config.algorithms,
95
190
  claims: config.claims,
96
191
  requiresTenant: config.requiresTenant,
192
+ authPosture: buildAuthPosture(workspaceRoot),
97
193
  workos,
98
194
  errors,
99
195
  },
@@ -116,6 +212,7 @@ function publicConfig(workspaceRoot: string): AuthCommandResult {
116
212
  algorithms: config.algorithms,
117
213
  claims: config.claims,
118
214
  requiresTenant: config.requiresTenant,
215
+ authPosture: buildAuthPosture(workspaceRoot),
119
216
  workos,
120
217
  },
121
218
  exitCode: 0,
@@ -197,6 +294,15 @@ async function testToken(
197
294
  export async function runAuthCommand(
198
295
  options: AuthCommandOptions,
199
296
  ): Promise<AuthCommandResult> {
297
+ if (options.subcommand === "status") {
298
+ const posture = buildAuthPosture(options.workspaceRoot);
299
+ return {
300
+ ok: true,
301
+ mode: posture.mode,
302
+ data: posture,
303
+ exitCode: 0,
304
+ };
305
+ }
200
306
  if (options.subcommand === "check") {
201
307
  return validateConfig(options.workspaceRoot);
202
308
  }
@@ -206,15 +312,34 @@ export async function runAuthCommand(
206
312
  const workos = detectWorkOS(options.workspaceRoot, config.claims);
207
313
  const productionMode = config.mode === "jwt" || config.mode === "oidc";
208
314
  const workosClaimsOk = workos.claimStatus.every((claim) => claim.ok);
315
+ const posture = buildAuthPosture(options.workspaceRoot);
316
+ const tokenProof = options.token ? await testToken(options.workspaceRoot, options.token) : null;
317
+ const prodError = options.prod && !productionMode
318
+ ? { code: "FORGE_AUTH_MODE_INVALID", message: "forge auth prove --prod requires FORGE_AUTH_MODE=jwt or oidc" }
319
+ : options.prod && !options.token
320
+ ? { code: "FORGE_AUTH_MISSING_TOKEN", message: "forge auth prove --prod requires --token" }
321
+ : options.prod && tokenProof && !tokenProof.ok
322
+ ? tokenProof.error
323
+ : undefined;
324
+ const proofOk = checked.ok && (!options.prod || (productionMode && tokenProof?.ok === true));
325
+ const multiTenantProof = options.scenario === "multi-tenant"
326
+ ? buildMultiTenantProof(options.workspaceRoot, workos, config.requiresTenant)
327
+ : null;
328
+ const scenarioOk = !multiTenantProof || multiTenantProof.ok;
209
329
  return {
210
- ok: checked.ok,
330
+ ok: proofOk && scenarioOk,
211
331
  mode: config.mode,
212
332
  data: {
213
333
  schemaVersion: "0.1.0",
214
334
  kind: "auth-proof",
215
- ok: checked.ok,
335
+ ok: proofOk,
216
336
  mode: config.mode,
217
337
  productionReady: productionMode && checked.ok,
338
+ prod: options.prod === true,
339
+ scenario: options.scenario ?? null,
340
+ authPosture: posture,
341
+ ...(tokenProof ? { tokenProof: tokenProof.ok ? tokenProof.data : tokenProof.error } : {}),
342
+ ...(multiTenantProof ? { multiTenantProof } : {}),
218
343
  invariants: [
219
344
  {
220
345
  id: "INV-001",
@@ -249,8 +374,8 @@ export async function runAuthCommand(
249
374
  workos,
250
375
  checkedAt: "deterministic",
251
376
  },
252
- error: checked.error,
253
- exitCode: checked.exitCode,
377
+ error: prodError ?? checked.error,
378
+ exitCode: proofOk && scenarioOk ? 0 : 1,
254
379
  };
255
380
  }
256
381
  if (options.subcommand === "config") {
@@ -0,0 +1,112 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createWorkspaceBaseline, readWorkspaceBaseline, writeWorkspaceBaseline, WORKSPACE_BASELINE_PATH } from "../workspace/baseline.ts";
3
+ import { listWorkspaceFiles } from "../workspace/git-summary.ts";
4
+
5
+ export type BaselineSubcommand = "create" | "status";
6
+
7
+ export interface BaselineCommandOptions {
8
+ subcommand: BaselineSubcommand;
9
+ workspaceRoot: string;
10
+ json: boolean;
11
+ reason?: string;
12
+ }
13
+
14
+ export interface BaselineCommandResult {
15
+ ok: boolean;
16
+ path: string;
17
+ required: boolean;
18
+ created?: boolean;
19
+ baseline?: ReturnType<typeof readWorkspaceBaseline>;
20
+ summary: {
21
+ files: number;
22
+ reason?: string;
23
+ tracking: "git" | "forge-baseline" | "missing";
24
+ };
25
+ nextActions: string[];
26
+ exitCode: 0 | 1;
27
+ }
28
+
29
+ function gitAvailable(workspaceRoot: string): boolean {
30
+ const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
31
+ cwd: workspaceRoot,
32
+ encoding: "utf8",
33
+ windowsHide: true,
34
+ });
35
+ return result.status === 0;
36
+ }
37
+
38
+ export function runBaselineCommand(options: BaselineCommandOptions): BaselineCommandResult {
39
+ if (options.subcommand === "create") {
40
+ const files = listWorkspaceFiles(options.workspaceRoot);
41
+ const baseline = createWorkspaceBaseline({
42
+ workspaceRoot: options.workspaceRoot,
43
+ files,
44
+ reason: options.reason,
45
+ });
46
+ writeWorkspaceBaseline(options.workspaceRoot, baseline);
47
+ return {
48
+ ok: true,
49
+ path: WORKSPACE_BASELINE_PATH,
50
+ required: false,
51
+ created: true,
52
+ baseline,
53
+ summary: {
54
+ files: Object.keys(baseline.files).length,
55
+ tracking: "forge-baseline",
56
+ ...(baseline.reason ? { reason: baseline.reason } : {}),
57
+ },
58
+ nextActions: ["forge changed --json", "forge handoff --json"],
59
+ exitCode: 0,
60
+ };
61
+ }
62
+
63
+ const baseline = readWorkspaceBaseline(options.workspaceRoot);
64
+ if (!baseline && gitAvailable(options.workspaceRoot)) {
65
+ return {
66
+ ok: true,
67
+ path: WORKSPACE_BASELINE_PATH,
68
+ required: false,
69
+ baseline: null,
70
+ summary: {
71
+ files: 0,
72
+ tracking: "git",
73
+ },
74
+ nextActions: ["forge changed --json", "git status --short"],
75
+ exitCode: 0,
76
+ };
77
+ }
78
+ return {
79
+ ok: baseline !== null,
80
+ path: WORKSPACE_BASELINE_PATH,
81
+ required: baseline === null,
82
+ baseline,
83
+ summary: {
84
+ files: baseline ? Object.keys(baseline.files).length : 0,
85
+ tracking: baseline ? "forge-baseline" : "missing",
86
+ ...(baseline?.reason ? { reason: baseline.reason } : {}),
87
+ },
88
+ nextActions: baseline
89
+ ? ["forge changed --json", "forge baseline create --reason refresh --json"]
90
+ : ["forge baseline create --reason initial-scaffold --json", "git init"],
91
+ exitCode: baseline ? 0 : 1,
92
+ };
93
+ }
94
+
95
+ export function formatBaselineJson(result: BaselineCommandResult): string {
96
+ return `${JSON.stringify(result, null, 2)}\n`;
97
+ }
98
+
99
+ export function formatBaselineHuman(result: BaselineCommandResult): string {
100
+ const label = result.required ? "missing" : result.summary.tracking === "git" ? "not required (git workspace)" : "ready";
101
+ const lines = [
102
+ `Forge baseline: ${label}`,
103
+ `Path: ${result.path}`,
104
+ `Tracking: ${result.summary.tracking}`,
105
+ `Files: ${result.summary.files}`,
106
+ ];
107
+ if (result.summary.reason) {
108
+ lines.push(`Reason: ${result.summary.reason}`);
109
+ }
110
+ lines.push("", "Next:", ...result.nextActions.map((action) => ` ${action}`));
111
+ return `${lines.join("\n")}\n`;
112
+ }