forgeos 0.1.0-alpha.31 → 0.1.0-alpha.33

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 (41) hide show
  1. package/AGENTS.md +3 -3
  2. package/CHANGELOG.md +27 -0
  3. package/docs/changelog.md +46 -1
  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 +74 -4
  9. package/src/forge/cli/changed.ts +38 -11
  10. package/src/forge/cli/commands.ts +71 -25
  11. package/src/forge/cli/db.ts +9 -8
  12. package/src/forge/cli/dev.ts +137 -56
  13. package/src/forge/cli/doctor.ts +3 -2
  14. package/src/forge/cli/handoff.ts +50 -3
  15. package/src/forge/cli/main.ts +3 -1
  16. package/src/forge/cli/new.ts +130 -12
  17. package/src/forge/cli/output.ts +94 -12
  18. package/src/forge/cli/parse.ts +13 -1
  19. package/src/forge/cli/studio.ts +15 -14
  20. package/src/forge/cli/windows.ts +2 -1
  21. package/src/forge/cli/workos.ts +5 -4
  22. package/src/forge/compiler/agent-contract/build.ts +3 -3
  23. package/src/forge/compiler/data-graph/sql/ddl.ts +11 -1
  24. package/src/forge/compiler/integration/templates/render.ts +1 -0
  25. package/src/forge/compiler/integration/templates/workos.ts +5 -5
  26. package/src/forge/compiler/make-registry/build.ts +2 -2
  27. package/src/forge/compiler/orchestrator/generate-lock.ts +14 -3
  28. package/src/forge/compiler/recipes/definitions.ts +1 -1
  29. package/src/forge/delta/status.ts +79 -12
  30. package/src/forge/dev/server.ts +4 -4
  31. package/src/forge/dev-console/cycle.ts +55 -20
  32. package/src/forge/dev-console/types.ts +1 -0
  33. package/src/forge/make/fields.ts +26 -0
  34. package/src/forge/make/index.ts +6 -1
  35. package/src/forge/runtime/db/memory-adapter.ts +139 -32
  36. package/src/forge/ui/index.ts +129 -0
  37. package/src/forge/version.ts +1 -1
  38. package/src/forge/workspace/change-summary.ts +7 -1
  39. package/src/forge/workspace/forge-cli.ts +29 -1
  40. package/templates/nuxt-web/web/package.json +2 -1
  41. package/templates/nuxt-web/web/tsconfig.json +4 -1
package/AGENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.31 input=ffdf0fea34e4880fa5215aaef174878f8b3e824def735dec6040c8a7502c4ae4 content=0d493cf0e41b71cb652d5e0e1b0c1f83d2a1281b748321f0b00f0773ba93074e
1
+ // @forge-generated generator=0.1.0-alpha.33 input=618472d83d84f1f735eeaaec13559692c94a492c7f592b20882b5ebac7a1c378 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,28 @@
1
1
  # forgeos
2
2
 
3
+ ## 0.1.0-alpha.33
4
+
5
+ ### Patch Changes
6
+
7
+ - Fix the Nuxt template smoke by adding the explicit `vue-tsc` dev dependency
8
+ required by `nuxt typecheck`, so newly scaffolded `nuxt-web` apps can run
9
+ `npm run typecheck` after install without manual package repair.
10
+
11
+ ## 0.1.0-alpha.32
12
+
13
+ ### Patch Changes
14
+
15
+ - Harden the alpha field-test loop after real Codex, WorkOS, PGlite, and app-server exercises.
16
+
17
+ - 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.
18
+ - Reject schema definitions that try to model `id` as a normal text field so generated SQL cannot silently create the wrong primary-key shape.
19
+ - 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.
20
+ - Improve smoke/generate drift diagnostics and keep `forge handoff` read-only with respect to generated artifacts.
21
+ - 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.
22
+ - 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`.
23
+ - Return structured JSON diagnostics for Delta/PGlite export/open failures instead of leaking raw PGlite stack output, and improve Windows/PGlite repair guidance.
24
+ - 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.
25
+
3
26
  ## 0.1.0-alpha.31
4
27
 
5
28
  ### Patch Changes
@@ -10,6 +33,10 @@
10
33
  - 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.
11
34
  - 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.
12
35
  - 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.
36
+ - 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.
37
+ - 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.
38
+ - 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.
39
+ - Allow `forge new .` / `npm create forgeos-app@alpha .` from an empty current directory, while refusing non-empty directories to avoid overwriting existing apps.
13
40
 
14
41
  ## 0.1.0-alpha.30
15
42
 
package/docs/changelog.md CHANGED
@@ -6,6 +6,38 @@ The canonical source file in the repository is `CHANGELOG.md`.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.1.0-alpha.33
10
+
11
+ - Fixed the Nuxt template smoke by adding the explicit `vue-tsc` dev
12
+ dependency required by `nuxt typecheck`, so newly scaffolded `nuxt-web` apps
13
+ can run `npm run typecheck` after install without manual package repair.
14
+
15
+ ## 0.1.0-alpha.32
16
+
17
+ - Accepted `create-forgeos-app --git` as a supported/no-op compatibility flag
18
+ when git initialization is already handled by the scaffold path, and kept
19
+ `npm create forgeos-app@alpha .` working from empty current directories.
20
+ - Added stronger schema validation for `id` so apps cannot accidentally model
21
+ the primary key as a normal text field and generate incorrect SQL.
22
+ - Fixed tenant-scoped updates in the in-memory database adapter and made memory
23
+ timestamp handling closer to PGlite by rejecting empty timestamp strings and
24
+ returning `Date` objects for timestamp columns.
25
+ - Improved smoke/generate drift diagnostics and kept `forge handoff` read-only
26
+ with respect to generated artifacts.
27
+ - Improved WorkOS/auth app DX: root-level `workos-seed.yml`, `HEAD /auth.md`,
28
+ `HEAD /.well-known/oauth-protected-resource`, and clearer local-vs-production
29
+ auth posture.
30
+ - Normalized suggested Forge commands in framework checkouts so `agent`,
31
+ `delta`, `studio`, `status`, `changed`, `handoff`, doctors, and WorkOS/auth
32
+ helpers recommend `node bin/forge.mjs ...` instead of a possibly stale global
33
+ `forge`.
34
+ - Returned structured JSON diagnostics for Delta/PGlite export/open failures
35
+ instead of raw PGlite stack output, and expanded Windows/PGlite repair hints.
36
+ - Expanded `forge ui audit` with static UX/auth-readiness warnings for missing
37
+ semantic landmarks, unlabeled form controls, unnamed buttons, missing
38
+ loading/error/empty states, and tenant/prod-auth apps that still only expose
39
+ local dev auth posture.
40
+
9
41
  ## 0.1.0-alpha.31
10
42
 
11
43
  - Added Forge workspace baselines for non-git app directories:
@@ -20,7 +52,20 @@ The canonical source file in the repository is `CHANGELOG.md`.
20
52
  exit code.
21
53
  - Added `forge ui audit` and wired it into `forge verify --smoke` for web apps
22
54
  to catch missing route scenarios, missing stable Forge test IDs, and missing
23
- policy-denied coverage for sensitive flows.
55
+ policy-denied coverage for sensitive flows. It now also reports static
56
+ UX/auth-readiness warnings for missing semantic landmarks, unlabeled form
57
+ controls, unnamed buttons, missing loading/error/empty states, and
58
+ tenant/prod-auth apps that still only expose local dev auth posture.
59
+ - Brought the in-memory DB adapter closer to PGlite for timestamp fields by
60
+ rejecting empty timestamp values and returning `Date` objects for
61
+ `timestamp`/`timestamptz` columns.
62
+ - Made `forge handoff` use a read-only generated-artifact check so preparing a
63
+ handoff no longer rewrites `src/forge/_generated/**`, `forge.lock`, or
64
+ generated `AGENTS.md` noise; `forge dev --once` still self-heals stale
65
+ artifacts.
66
+ - Allowed `forge new .` / `npm create forgeos-app@alpha .` from an empty
67
+ current directory, while refusing non-empty directories to avoid overwriting
68
+ existing apps.
24
69
 
25
70
  ## 0.1.0-alpha.30
26
71
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgeos",
3
- "version": "0.1.0-alpha.31",
3
+ "version": "0.1.0-alpha.33",
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.31","releaseId":"forgeos@0.1.0-alpha.31+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.33","releaseId":"forgeos@0.1.0-alpha.33+unknown","schemaVersion":"0.1.0"}
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.31 input=ffdf0fea34e4880fa5215aaef174878f8b3e824def735dec6040c8a7502c4ae4 content=806e4e7f20264628107bf964331f13335175f065d6a8826b5defc2027d8047cd
1
+ // @forge-generated generator=0.1.0-alpha.33 input=618472d83d84f1f735eeaaec13559692c94a492c7f592b20882b5ebac7a1c378 content=2cfb24f552edc1fade82dc4abca3f6085ae94a0ac9ae70f5b0cffab11c1c3e7c
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.31",
23
- "releaseId": "forgeos@0.1.0-alpha.31+unknown",
22
+ "packageVersion": "0.1.0-alpha.33",
23
+ "releaseId": "forgeos@0.1.0-alpha.33+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,6 +9,9 @@ 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
16
  export type AuthSubcommand = "check" | "config" | "decode" | "test-token" | "jwks" | "prove" | "status";
14
17
 
@@ -18,6 +21,7 @@ export interface AuthCommandOptions {
18
21
  json: boolean;
19
22
  token?: string;
20
23
  prod?: boolean;
24
+ scenario?: string;
21
25
  }
22
26
 
23
27
  export interface AuthCommandResult {
@@ -88,7 +92,7 @@ function buildAuthPosture(workspaceRoot: string) {
88
92
  const productionMode = config.mode === "jwt" || config.mode === "oidc";
89
93
  const errors = configErrors(config);
90
94
  const configReady = errors.length === 0;
91
- return {
95
+ return normalizeForgeCliCommandsInValue(workspaceRoot, {
92
96
  schemaVersion: "0.1.0",
93
97
  mode: config.mode,
94
98
  localOnly: config.mode === "dev-headers",
@@ -96,6 +100,15 @@ function buildAuthPosture(workspaceRoot: string) {
96
100
  requiresTenant: config.requiresTenant,
97
101
  bearerHeader: productionMode ? "Authorization: Bearer <token>" : null,
98
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
+ ],
99
112
  reason: productionMode
100
113
  ? configReady
101
114
  ? "jwt/oidc production auth configuration is present"
@@ -106,7 +119,58 @@ function buildAuthPosture(workspaceRoot: string) {
106
119
  ? ["forge auth prove --prod --token <jwt> --json", "forge serve --json"]
107
120
  : ["forge auth check --json", "configure FORGE_AUTH_ISSUER, FORGE_AUTH_AUDIENCE, and FORGE_AUTH_JWKS_URI or OIDC issuer"]
108
121
  : ["set FORGE_AUTH_MODE=jwt or oidc for production", "forge auth prove --prod --token <jwt> --json"],
109
- };
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
+ });
110
174
  }
111
175
 
112
176
  function validateConfig(workspaceRoot: string): AuthCommandResult {
@@ -258,8 +322,12 @@ export async function runAuthCommand(
258
322
  ? tokenProof.error
259
323
  : undefined;
260
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;
261
329
  return {
262
- ok: proofOk,
330
+ ok: proofOk && scenarioOk,
263
331
  mode: config.mode,
264
332
  data: {
265
333
  schemaVersion: "0.1.0",
@@ -268,8 +336,10 @@ export async function runAuthCommand(
268
336
  mode: config.mode,
269
337
  productionReady: productionMode && checked.ok,
270
338
  prod: options.prod === true,
339
+ scenario: options.scenario ?? null,
271
340
  authPosture: posture,
272
341
  ...(tokenProof ? { tokenProof: tokenProof.ok ? tokenProof.data : tokenProof.error } : {}),
342
+ ...(multiTenantProof ? { multiTenantProof } : {}),
273
343
  invariants: [
274
344
  {
275
345
  id: "INV-001",
@@ -305,7 +375,7 @@ export async function runAuthCommand(
305
375
  checkedAt: "deterministic",
306
376
  },
307
377
  error: prodError ?? checked.error,
308
- exitCode: proofOk ? 0 : 1,
378
+ exitCode: proofOk && scenarioOk ? 0 : 1,
309
379
  };
310
380
  }
311
381
  if (options.subcommand === "config") {
@@ -4,6 +4,7 @@ import {
4
4
  filterCategorizedSummary,
5
5
  summarizeChangeTypes,
6
6
  } from "../workspace/change-summary.ts";
7
+ import { forgeCliCommandsForWorkspace } from "../workspace/forge-cli.ts";
7
8
  import { buildWorkspaceGitSummary, type WorkspaceGitSummary } from "../workspace/git-summary.ts";
8
9
 
9
10
  export interface ChangedCommandResult {
@@ -44,6 +45,7 @@ interface GeneratedChangeExplanation {
44
45
  }
45
46
 
46
47
  const AUTHORED_CHANGE_TYPES = ["source", "tests", "docs", "config", "assets", "other"] as const;
48
+ const REVIEW_CHANGE_TYPES = ["source", "tests", "docs", "config", "assets"] as const;
47
49
 
48
50
  function emptyCategory(summary: CategorizedFileSummary, category: keyof CategorizedFileSummary["byType"]): boolean {
49
51
  return summary.byType[category].count === 0;
@@ -102,10 +104,16 @@ function buildRisks(git: WorkspaceGitSummary): string[] {
102
104
  if (!emptyCategory(changed, "generated") && humanChanges.total === 0) {
103
105
  risks.push("only generated artifacts changed; verify the source edit, generator input, or intentional regeneration that produced them");
104
106
  }
107
+ return risks;
108
+ }
109
+
110
+ function buildAdvisories(git: WorkspaceGitSummary): string[] {
111
+ const advisories: string[] = [];
112
+ const changed = git.changeSummary.changed;
105
113
  if (changed.total.count > 50) {
106
- risks.push(`${changed.total.count} changed file(s) detected; use the grouped summaries before reviewing raw diffs`);
114
+ advisories.push(`${changed.total.count} changed file(s) detected; use grouped summaries before reviewing raw diffs`);
107
115
  }
108
- return risks;
116
+ return advisories;
109
117
  }
110
118
 
111
119
  function buildRecommendedCommands(git: WorkspaceGitSummary): string[] {
@@ -127,7 +135,12 @@ function buildRecommendedCommands(git: WorkspaceGitSummary): string[] {
127
135
  "forge generate --check --json",
128
136
  ];
129
137
  }
138
+ const authoredGeneratedInputs =
139
+ changed.byType.source.count +
140
+ changed.byType.config.count +
141
+ changed.byType.operational.count;
130
142
  return [
143
+ ...(authoredGeneratedInputs > 0 ? ["forge generate --check --json"] : []),
131
144
  "forge handoff --json",
132
145
  "forge test plan --changed --json",
133
146
  "forge verify --changed",
@@ -189,7 +202,7 @@ function buildGeneratedChangeExplanation(
189
202
  };
190
203
  }
191
204
 
192
- export function runChangedCommand(workspaceRoot: string, options: { authoredOnly?: boolean } = {}): ChangedCommandResult {
205
+ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly?: boolean; reviewOnly?: boolean } = {}): ChangedCommandResult {
193
206
  const git = buildWorkspaceGitSummary(workspaceRoot);
194
207
  const changed = git.changeSummary.changed;
195
208
  const humanChanges = selectHumanChangeSummary(changed);
@@ -198,16 +211,25 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
198
211
  const authoredStaged = filterCategorizedSummary(git.changeSummary.staged, [...AUTHORED_CHANGE_TYPES]);
199
212
  const authoredUnstaged = filterCategorizedSummary(git.changeSummary.unstaged, [...AUTHORED_CHANGE_TYPES]);
200
213
  const authoredUntracked = filterCategorizedSummary(git.changeSummary.untracked, [...AUTHORED_CHANGE_TYPES]);
201
- const viewHumanChanges = options.authoredOnly ? selectHumanChangeSummary(authoredChanged) : humanChanges;
202
- const viewChanged = options.authoredOnly ? authoredChanged : changed;
203
- const viewStaged = options.authoredOnly ? authoredStaged : git.changeSummary.staged;
204
- const viewUnstaged = options.authoredOnly ? authoredUnstaged : git.changeSummary.unstaged;
205
- const viewUntracked = options.authoredOnly ? authoredUntracked : git.changeSummary.untracked;
206
- const viewDerivedChanges: DerivedChangeSummary = options.authoredOnly
214
+ const reviewChanged = filterCategorizedSummary(changed, [...REVIEW_CHANGE_TYPES]);
215
+ const reviewStaged = filterCategorizedSummary(git.changeSummary.staged, [...REVIEW_CHANGE_TYPES]);
216
+ const reviewUnstaged = filterCategorizedSummary(git.changeSummary.unstaged, [...REVIEW_CHANGE_TYPES]);
217
+ const reviewUntracked = filterCategorizedSummary(git.changeSummary.untracked, [...REVIEW_CHANGE_TYPES]);
218
+ const viewHumanChanges = options.reviewOnly
219
+ ? selectHumanChangeSummary(reviewChanged)
220
+ : options.authoredOnly
221
+ ? selectHumanChangeSummary(authoredChanged)
222
+ : humanChanges;
223
+ const viewChanged = options.reviewOnly ? reviewChanged : options.authoredOnly ? authoredChanged : changed;
224
+ const viewStaged = options.reviewOnly ? reviewStaged : options.authoredOnly ? authoredStaged : git.changeSummary.staged;
225
+ const viewUnstaged = options.reviewOnly ? reviewUnstaged : options.authoredOnly ? authoredUnstaged : git.changeSummary.unstaged;
226
+ const viewUntracked = options.reviewOnly ? reviewUntracked : options.authoredOnly ? authoredUntracked : git.changeSummary.untracked;
227
+ const viewDerivedChanges: DerivedChangeSummary = options.authoredOnly || options.reviewOnly
207
228
  ? { total: 0, generated: { count: 0, sample: [], hidden: 0 } }
208
229
  : derivedChanges;
209
230
  const risks = buildRisks(git);
210
- const recommendedCommands = buildRecommendedCommands(git);
231
+ const advisories = buildAdvisories(git);
232
+ const recommendedCommands = forgeCliCommandsForWorkspace(workspaceRoot, buildRecommendedCommands(git));
211
233
  const reviewFocus = buildReviewFocus(viewHumanChanges, viewDerivedChanges);
212
234
  const generatedExplanation = buildGeneratedChangeExplanation(viewHumanChanges, viewDerivedChanges);
213
235
  const diffPlan: DiffPlan = buildDiffPlanFromChangeSummary(viewChanged);
@@ -223,7 +245,7 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
223
245
  commit: git.commit,
224
246
  workspaceMode: git.workspaceMode ?? (git.available ? "git" : "nonGit"),
225
247
  tracking: git.tracking ?? git.source,
226
- view: options.authoredOnly ? "authored" : "all",
248
+ view: options.reviewOnly ? "review" : options.authoredOnly ? "authored" : "all",
227
249
  changedFiles: viewChanged.total.count,
228
250
  humanFiles: viewHumanChanges.total,
229
251
  generatedFiles: viewDerivedChanges.total,
@@ -253,6 +275,7 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
253
275
  generatedExplanation,
254
276
  diffPlan,
255
277
  risks,
278
+ advisories,
256
279
  recommendedCommands,
257
280
  nextActions: recommendedCommands,
258
281
  },
@@ -268,6 +291,7 @@ export function formatChangedHuman(result: ChangedCommandResult): string {
268
291
  const generatedExplanation = result.data.generatedExplanation as { summary?: string } | undefined;
269
292
  const diffPlan = result.data.diffPlan as { summary?: string; authoredDiffCommand?: string; generatedDiffCommand?: string; generatedCollapsedByDefault?: boolean } | undefined;
270
293
  const risks = (result.data.risks as string[] | undefined) ?? [];
294
+ const advisories = (result.data.advisories as string[] | undefined) ?? [];
271
295
  const nextActions = (result.data.nextActions as string[] | undefined) ?? [];
272
296
  const lines = [
273
297
  `Forge changed: ${result.ok ? "ready" : "git unavailable"}`,
@@ -310,6 +334,9 @@ export function formatChangedHuman(result: ChangedCommandResult): string {
310
334
  if (risks.length > 0) {
311
335
  lines.push("", "Risks:", ...risks.map((risk) => ` ${risk}`));
312
336
  }
337
+ if (advisories.length > 0) {
338
+ lines.push("", "Notes:", ...advisories.map((advisory) => ` ${advisory}`));
339
+ }
313
340
 
314
341
  lines.push("", "Next:", ...nextActions.map((command) => ` ${command}`));
315
342
  return `${lines.join("\n")}\n`;